Completed
Push — development ( ac22bd...5236fa )
by Stephen
14:46
created

Topic.subs.php ➔ moveTopicConcurrence()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 3
nop 3
dl 0
loc 36
ccs 0
cts 19
cp 0
crap 30
rs 8.439
c 0
b 0
f 0
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
 * @name      ElkArte Forum
10
 * @copyright ElkArte Forum contributors
11
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
12
 *
13
 * This file contains code covered by:
14
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
15
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
16
 *
17
 * @version 1.1
18
 *
19
 */
20
21
/**
22
 * Removes the passed id_topic's checking for permissions.
23
 *
24
 * @param int[]|int $topics The topics to remove (can be an id or an array of ids).
25
 * @throws Elk_Exception
26
 */
27
function removeTopicsPermissions($topics)
28
{
29
	global $board, $user_info;
30
31
	// They can only delete their own topics. (we wouldn't be here if they couldn't do that..)
32
	$possible_remove = topicAttribute($topics, array('id_topic', 'id_board', 'id_member_started'));
33
34
	$removeCache = array();
35
	$removeCacheBoards = array();
36
	$test_owner = !empty($board) && !allowedTo('remove_any');
37
	foreach ($possible_remove as $row)
38
	{
39
		// Skip if we have to test the owner *and* the user is not the owner
40
		if ($test_owner && $row['id_member_started'] != $user_info['id'])
41
			continue;
42
43
		$removeCache[] = $row['id_topic'];
44
		$removeCacheBoards[$row['id_topic']] = $row['id_board'];
45
	}
46
47
	// Maybe *none* were their own topics.
48
	if (!empty($removeCache))
49
		removeTopics($removeCache, true, false, true, $removeCacheBoards);
50
}
51
52
/**
53
 * Removes the passed id_topic's.
54
 *
55
 * - Permissions are NOT checked here because the function is used in a scheduled task
56
 *
57
 * @param int[]|int $topics The topics to remove (can be an id or an array of ids).
58
 * @param bool $decreasePostCount if true users' post count will be reduced
59
 * @param bool $ignoreRecycling if true topics are not moved to the recycle board (if it exists).
60
 * @param bool $log if true logs the action.
61
 * @param int[] $removeCacheBoards an array matching topics and boards.
62
 * @throws Elk_Exception
63
 */
64
function removeTopics($topics, $decreasePostCount = true, $ignoreRecycling = false, $log = false, $removeCacheBoards = array())
65
{
66 36
	global $modSettings;
67
68
	// Nothing to do?
69 36
	if (empty($topics))
70 24
		return;
71
72 36
	$db = database();
73 36
	$cache = Cache::instance();
74
75
	// Only a single topic.
76 36
	if (!is_array($topics))
77 36
		$topics = array($topics);
78
79 12
	if ($log)
80 24
	{
81
		// Gotta send the notifications *first*!
82
		foreach ($topics as $topic)
83
		{
84
			// Only log the topic ID if it's not in the recycle board.
85
			logAction('remove', array((empty($modSettings['recycle_enable']) || $modSettings['recycle_board'] != $removeCacheBoards[$topic] ? 'topic' : 'old_topic_id') => $topic, 'board' => $removeCacheBoards[$topic]));
86
			sendNotifications($topic, 'remove');
87
		}
88
	}
89
90
	// Decrease the post counts for members.
91 12
	if ($decreasePostCount)
92 24
	{
93 36
		$requestMembers = $db->query('', '
94
			SELECT m.id_member, COUNT(*) AS posts
95
			FROM {db_prefix}messages AS m
96
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
97
			WHERE m.id_topic IN ({array_int:topics})
98
				AND m.icon != {string:recycled}
99
				AND b.count_posts = {int:do_count_posts}
100
				AND m.approved = {int:is_approved}
101 24
			GROUP BY m.id_member',
102
			array(
103 36
				'do_count_posts' => 0,
104 36
				'recycled' => 'recycled',
105 36
				'topics' => $topics,
106 36
				'is_approved' => 1,
107
			)
108 24
		);
109 36
		if ($db->num_rows($requestMembers) > 0)
110 24
		{
111 36
			require_once(SUBSDIR . '/Members.subs.php');
112 36
			while ($rowMembers = $db->fetch_assoc($requestMembers))
113 36
				updateMemberData($rowMembers['id_member'], array('posts' => 'posts - ' . $rowMembers['posts']));
114 24
		}
115 36
		$db->free_result($requestMembers);
116 24
	}
117
118
	// Recycle topics that aren't in the recycle board...
119 36
	if (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 && !$ignoreRecycling)
120 24
	{
121
		$possible_recycle = topicAttribute($topics, array('id_topic', 'id_board', 'unapproved_posts', 'approved'));
122
123
		if (!empty($possible_recycle))
124
		{
125
			detectServer()->setTimeLimit(300);
126
127
			// Get topics that will be recycled.
128
			$recycleTopics = array();
129
			foreach ($possible_recycle as $row)
130
			{
131
				// If it's already in the recycle board do nothing
132
				if ($row['id_board'] == $modSettings['recycle_board'])
133
					continue;
134
135
				$recycleTopics[] = $row['id_topic'];
136
137
				// Set the id_previous_board for this topic - and make it not sticky.
138
				setTopicAttribute($row['id_topic'], array(
139
					'id_previous_board' => $row['id_board'],
140
					'is_sticky' => 0,
141
				));
142
			}
143
144
			if (!empty($recycleTopics))
145
			{
146
				// Mark recycled topics as recycled.
147
				$db->query('', '
148
				UPDATE {db_prefix}messages
149
				SET icon = {string:recycled}
150
				WHERE id_topic IN ({array_int:recycle_topics})',
151
					array(
152
						'recycle_topics' => $recycleTopics,
153
						'recycled' => 'recycled',
154
					)
155
				);
156
157
				// Move the topics to the recycle board.
158
				require_once(SUBSDIR . '/Topic.subs.php');
159
				moveTopics($recycleTopics, $modSettings['recycle_board']);
160
161
				// Close reports that are being recycled.
162
				require_once(SUBSDIR . '/Moderation.subs.php');
163
164
				$db->query('', '
165
				UPDATE {db_prefix}log_reported
166
				SET closed = {int:is_closed}
167
				WHERE id_topic IN ({array_int:recycle_topics})',
168
					array(
169
						'recycle_topics' => $recycleTopics,
170
						'is_closed' => 1,
171
					)
172
				);
173
174
				updateSettings(array('last_mod_report_action' => time()));
175
				recountOpenReports();
176
177
				// Topics that were recycled don't need to be deleted, so subtract them.
178
				$topics = array_diff($topics, $recycleTopics);
179
			}
180
		}
181
	}
182
183
	// Still topics left to delete?
184 36
	if (empty($topics))
185 24
		return;
186
187 36
	$adjustBoards = array();
188
189
	// Find out how many posts we are deleting.
190 36
	$request = $db->query('', '
191
		SELECT id_board, approved, COUNT(*) AS num_topics, SUM(unapproved_posts) AS unapproved_posts,
192
			SUM(num_replies) AS num_replies
193
		FROM {db_prefix}topics
194
		WHERE id_topic IN ({array_int:topics})
195 24
		GROUP BY id_board, approved',
196
		array(
197 36
			'topics' => $topics,
198
		)
199 24
	);
200 36 View Code Duplication
	while ($row = $db->fetch_assoc($request))
201
	{
202 36
		if (!isset($adjustBoards[$row['id_board']]['num_posts']))
203 24
		{
204 36
			$cache->remove('board-' . $row['id_board']);
205
206 36
			$adjustBoards[$row['id_board']] = array(
207 36
				'num_posts' => 0,
208 36
				'num_topics' => 0,
209 36
				'unapproved_posts' => 0,
210 36
				'unapproved_topics' => 0,
211 36
				'id_board' => $row['id_board']
212 24
			);
213 24
		}
214
		// Posts = (num_replies + 1) for each approved topic.
215 36
		$adjustBoards[$row['id_board']]['num_posts'] += $row['num_replies'] + ($row['approved'] ? $row['num_topics'] : 0);
216 36
		$adjustBoards[$row['id_board']]['unapproved_posts'] += $row['unapproved_posts'];
217
218
		// Add the topics to the right type.
219 36
		if ($row['approved'])
220 36
			$adjustBoards[$row['id_board']]['num_topics'] += $row['num_topics'];
221
		else
222
			$adjustBoards[$row['id_board']]['unapproved_topics'] += $row['num_topics'];
223 24
	}
224 36
	$db->free_result($request);
225
226
	// Decrease number of posts and topics for each board.
227 36
	detectServer()->setTimeLimit(300);
228 36 View Code Duplication
	foreach ($adjustBoards as $stats)
229
	{
230 36
		$db->query('', '
231
			UPDATE {db_prefix}boards
232
			SET
233
				num_posts = CASE WHEN {int:num_posts} > num_posts THEN 0 ELSE num_posts - {int:num_posts} END,
234
				num_topics = CASE WHEN {int:num_topics} > num_topics THEN 0 ELSE num_topics - {int:num_topics} END,
235
				unapproved_posts = CASE WHEN {int:unapproved_posts} > unapproved_posts THEN 0 ELSE unapproved_posts - {int:unapproved_posts} END,
236
				unapproved_topics = CASE WHEN {int:unapproved_topics} > unapproved_topics THEN 0 ELSE unapproved_topics - {int:unapproved_topics} END
237 24
			WHERE id_board = {int:id_board}',
238
			array(
239 36
				'id_board' => $stats['id_board'],
240 36
				'num_posts' => $stats['num_posts'],
241 36
				'num_topics' => $stats['num_topics'],
242 36
				'unapproved_posts' => $stats['unapproved_posts'],
243 36
				'unapproved_topics' => $stats['unapproved_topics'],
244
			)
245 24
		);
246 24
	}
247
248
	// Remove polls for these topics.
249 36
	$possible_polls = topicAttribute($topics, 'id_poll');
250 36
	$polls = array();
251 36
	foreach ($possible_polls as $row)
252
	{
253 36
		if (!empty($row['id_poll']))
254 36
			$polls[] = $row['id_poll'];
255 24
	}
256
257 36
	if (!empty($polls))
258 24
	{
259 18
		$db->query('', '
260
			DELETE FROM {db_prefix}polls
261 12
			WHERE id_poll IN ({array_int:polls})',
262
			array(
263 18
				'polls' => $polls,
264
			)
265 12
		);
266 18
		$db->query('', '
267
			DELETE FROM {db_prefix}poll_choices
268 12
			WHERE id_poll IN ({array_int:polls})',
269
			array(
270 18
				'polls' => $polls,
271
			)
272 12
		);
273 18
		$db->query('', '
274
			DELETE FROM {db_prefix}log_polls
275 12
			WHERE id_poll IN ({array_int:polls})',
276
			array(
277 18
				'polls' => $polls,
278
			)
279 12
		);
280 12
	}
281
282
	// Get rid of the attachment(s).
283 36
	require_once(SUBSDIR . '/ManageAttachments.subs.php');
284
	$attachmentQuery = array(
285 36
		'attachment_type' => 0,
286 36
		'id_topic' => $topics,
287 24
	);
288 36
	removeAttachments($attachmentQuery, 'messages');
289
290
	// Delete search index entries.
291 36
	if (!empty($modSettings['search_custom_index_config']))
292 24
	{
293
		$customIndexSettings = Util::unserialize($modSettings['search_custom_index_config']);
294
295
		$request = $db->query('', '
296
			SELECT id_msg, body
297
			FROM {db_prefix}messages
298
			WHERE id_topic IN ({array_int:topics})',
299
			array(
300
				'topics' => $topics,
301
			)
302
		);
303
		$words = array();
304
		$messages = array();
305
		while ($row = $db->fetch_assoc($request))
306
		{
307
			detectServer()->setTimeLimit(300);
308
309
			$words = array_merge($words, text2words($row['body'], $customIndexSettings['bytes_per_word'], true));
310
			$messages[] = $row['id_msg'];
311
		}
312
		$db->free_result($request);
313
		$words = array_unique($words);
314
315
		if (!empty($words) && !empty($messages))
316
			$db->query('', '
317
				DELETE FROM {db_prefix}log_search_words
318
				WHERE id_word IN ({array_int:word_list})
319
					AND id_msg IN ({array_int:message_list})',
320
				array(
321
					'word_list' => $words,
322
					'message_list' => $messages,
323
				)
324
			);
325
	}
326
327
	// Reuse the message array if available
328 36
	if (empty($messages))
329 36
		$messages = messagesInTopics($topics);
330
331
	// If there are messages left in this topic
332 36
	if (!empty($messages))
333 24
	{
334
		// Decrease / Update the member like counts
335 36
		require_once(SUBSDIR . '/Likes.subs.php');
336 36
		decreaseLikeCounts($messages);
337
338
		// Remove all likes now that the topic is gone
339 36
		$db->query('', '
340
			DELETE FROM {db_prefix}message_likes
341 24
			WHERE id_msg IN ({array_int:messages})',
342
			array(
343 36
				'messages' => $messages,
344
			)
345 24
		);
346
347
		// Remove all mentions now that the topic is gone
348 36
		$db->query('', '
349
			DELETE FROM {db_prefix}log_mentions
350
			WHERE id_target IN ({array_int:messages})
351 24
				AND mention_type IN ({array_string:mension_types})',
352
			array(
353 36
				'messages' => $messages,
354 24
				'mension_types' => array('mentionmem', 'likemsg', 'rlikemsg'),
355
			)
356 24
		);
357 24
	}
358
359
	// Delete messages in each topic.
360 36
	$db->query('', '
361
		DELETE FROM {db_prefix}messages
362 24
		WHERE id_topic IN ({array_int:topics})',
363
		array(
364 36
			'topics' => $topics,
365
		)
366 24
	);
367
368
	// Remove linked calendar events.
369
	// @todo if unlinked events are enabled, wouldn't this be expected to keep them?
370 36
	$db->query('', '
371
		DELETE FROM {db_prefix}calendar
372 24
		WHERE id_topic IN ({array_int:topics})',
373
		array(
374 36
			'topics' => $topics,
375
		)
376 24
	);
377
378
	// Delete log_topics data
379 36
	$db->query('', '
380
		DELETE FROM {db_prefix}log_topics
381 24
		WHERE id_topic IN ({array_int:topics})',
382
		array(
383 36
			'topics' => $topics,
384
		)
385 24
	);
386
387
	// Delete notifications
388 36
	$db->query('', '
389
		DELETE FROM {db_prefix}log_notify
390 24
		WHERE id_topic IN ({array_int:topics})',
391
		array(
392 36
			'topics' => $topics,
393
		)
394 24
	);
395
396
	// Delete the topics themselves
397 36
	$db->query('', '
398
		DELETE FROM {db_prefix}topics
399 24
		WHERE id_topic IN ({array_int:topics})',
400
		array(
401 36
			'topics' => $topics,
402
		)
403 24
	);
404
405
	// Remove data from the subjects for search cache
406 36
	$db->query('', '
407
		DELETE FROM {db_prefix}log_search_subjects
408 24
		WHERE id_topic IN ({array_int:topics})',
409
		array(
410 36
			'topics' => $topics,
411
		)
412 24
	);
413 36
	require_once(SUBSDIR . '/FollowUps.subs.php');
414 36
	removeFollowUpsByTopic($topics);
415
416 36
	foreach ($topics as $topic_id)
417 36
		$cache->remove('topic_board-' . $topic_id);
418
419
	// Maybe there's an addon that wants to delete topic related data of its own
420 36
	call_integration_hook('integrate_remove_topics', array($topics));
421
422
	// Update the totals...
423 36
	require_once(SUBSDIR . '/Messages.subs.php');
424 36
	updateMessageStats();
425 36
	updateTopicStats();
426 36
	updateSettings(array(
427 36
		'calendar_updated' => time(),
428 24
	));
429
430 36
	require_once(SUBSDIR . '/Post.subs.php');
431 36
	$updates = array();
432 36
	foreach ($adjustBoards as $stats)
433 36
		$updates[] = $stats['id_board'];
434 36
	updateLastMessages($updates);
435 36
}
436
437
/**
438
 * Moves lots of topics to a specific board and checks if the user can move them
439
 *
440
 * @param array $moveCache [0] => int[] is the topic, [1] => int[]  is the board to move to.
441
 * @throws Elk_Exception
442
 */
443
function moveTopicsPermissions($moveCache)
444
{
445
	global $board, $user_info;
446
447
	$db = database();
448
449
	// I know - I just KNOW you're trying to beat the system.  Too bad for you... we CHECK :P.
450
	$request = $db->query('', '
451
		SELECT t.id_topic, t.id_board, b.count_posts
452
		FROM {db_prefix}topics AS t
453
			LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
454
		WHERE t.id_topic IN ({array_int:move_topic_ids})' . (!empty($board) && !allowedTo('move_any') ? '
455
			AND t.id_member_started = {int:current_member}' : '') . '
456
		LIMIT ' . count($moveCache[0]),
457
		array(
458
			'current_member' => $user_info['id'],
459
			'move_topic_ids' => $moveCache[0],
460
		)
461
	);
462
	$moveTos = array();
463
	$moveCache2 = array();
464
	$countPosts = array();
465
	while ($row = $db->fetch_assoc($request))
466
	{
467
		$to = $moveCache[1][$row['id_topic']];
468
469
		if (empty($to))
470
			continue;
471
472
		// Does this topic's board count the posts or not?
473
		$countPosts[$row['id_topic']] = empty($row['count_posts']);
474
475
		if (!isset($moveTos[$to]))
476
			$moveTos[$to] = array();
477
478
		$moveTos[$to][] = $row['id_topic'];
479
480
		// For reporting...
481
		$moveCache2[] = array($row['id_topic'], $row['id_board'], $to);
482
	}
483
	$db->free_result($request);
484
485
	// Do the actual moves...
486
	foreach ($moveTos as $to => $topics)
487
		moveTopics($topics, $to, true);
488
489
	// Does the post counts need to be updated?
490
	if (!empty($moveTos))
491
	{
492
		require_once(SUBSDIR . '/Boards.subs.php');
493
		$topicRecounts = array();
494
		$boards_info = fetchBoardsInfo(array('boards' => array_keys($moveTos)), array('selects' => 'posts'));
495
496
		foreach ($boards_info as $row)
497
		{
498
			$cp = empty($row['count_posts']);
499
500
			// Go through all the topics that are being moved to this board.
501
			foreach ($moveTos[$row['id_board']] as $topic)
502
			{
503
				// If both boards have the same value for post counting then no adjustment needs to be made.
504
				if ($countPosts[$topic] != $cp)
505
				{
506
					// If the board being moved to does count the posts then the other one doesn't so add to their post count.
507
					$topicRecounts[$topic] = $cp ? 1 : -1;
508
				}
509
			}
510
		}
511
512
		if (!empty($topicRecounts))
513
		{
514
			require_once(SUBSDIR . '/Members.subs.php');
515
516
			// Get all the members who have posted in the moved topics.
517
			$posters = topicsPosters(array_keys($topicRecounts));
518
			foreach ($posters as $id_member => $topics)
519
			{
520
				$post_adj = 0;
521
				foreach ($topics as $id_topic)
522
					$post_adj += $topicRecounts[$id_topic];
523
524
				// And now update that member's post counts
525
				if (!empty($post_adj))
526
				{
527
					updateMemberData($id_member, array('posts' => 'posts + ' . $post_adj));
528
				}
529
			}
530
		}
531
	}
532
}
533
534
/**
535
 * Moves one or more topics to a specific board.
536
 *
537
 * What it does:
538
 *
539
 * - Determines the source boards for the supplied topics
540
 * - Handles the moving of mark_read data
541
 * - Updates the posts count of the affected boards
542
 * - This function doesn't check permissions.
543
 *
544
 * @param int[]|int $topics
545
 * @param int $toBoard
546
 * @param bool $log if true logs the action.
547
 * @throws Elk_Exception
548
 */
549
function moveTopics($topics, $toBoard, $log = false)
550
{
551
	global $user_info, $modSettings;
552
553
	// No topics or no board?
554
	if (empty($topics) || empty($toBoard))
555
		return;
556
557
	$db = database();
558
559
	// Only a single topic.
560
	if (!is_array($topics))
561
		$topics = array($topics);
562
563
	$fromBoards = array();
564
	$fromCacheBoards = array();
565
566
	// Are we moving to the recycle board?
567
	$isRecycleDest = !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $toBoard;
568
569
	// Determine the source boards...
570
	$request = $db->query('', '
571
		SELECT id_topic, id_board, approved, COUNT(*) AS num_topics, SUM(unapproved_posts) AS unapproved_posts,
572
			SUM(num_replies) AS num_replies
573
		FROM {db_prefix}topics
574
		WHERE id_topic IN ({array_int:topics})
575
		GROUP BY id_board, approved',
576
		array(
577
			'topics' => $topics,
578
		)
579
	);
580
	// Num of rows = 0 -> no topics found. Num of rows > 1 -> topics are on multiple boards.
581
	if ($db->num_rows($request) == 0)
582
		return;
583
584 View Code Duplication
	while ($row = $db->fetch_assoc($request))
585
	{
586
		$fromCacheBoards[$row['id_topic']] = $row['id_board'];
587
		if (!isset($fromBoards[$row['id_board']]['num_posts']))
588
		{
589
			$fromBoards[$row['id_board']] = array(
590
				'num_posts' => 0,
591
				'num_topics' => 0,
592
				'unapproved_posts' => 0,
593
				'unapproved_topics' => 0,
594
				'id_board' => $row['id_board']
595
			);
596
		}
597
		// Posts = (num_replies + 1) for each approved topic.
598
		$fromBoards[$row['id_board']]['num_posts'] += $row['num_replies'] + ($row['approved'] ? $row['num_topics'] : 0);
599
		$fromBoards[$row['id_board']]['unapproved_posts'] += $row['unapproved_posts'];
600
601
		// Add the topics to the right type.
602
		if ($row['approved'])
603
			$fromBoards[$row['id_board']]['num_topics'] += $row['num_topics'];
604
		else
605
			$fromBoards[$row['id_board']]['unapproved_topics'] += $row['num_topics'];
606
	}
607
	$db->free_result($request);
608
609
	// Move over the mark_read data. (because it may be read and now not by some!)
610
	$SaveAServer = max(0, $modSettings['maxMsgID'] - 50000);
611
	$request = $db->query('', '
612
		SELECT lmr.id_member, lmr.id_msg, t.id_topic, COALESCE(lt.unwatched, 0) as unwatched
613
		FROM {db_prefix}topics AS t
614
			INNER JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board
615
				AND lmr.id_msg > t.id_first_msg AND lmr.id_msg > {int:protect_lmr_msg})
616
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = lmr.id_member)
617
		WHERE t.id_topic IN ({array_int:topics})
618
			AND lmr.id_msg > COALESCE(lt.id_msg, 0)',
619
		array(
620
			'protect_lmr_msg' => $SaveAServer,
621
			'topics' => $topics,
622
		)
623
	);
624
	$log_topics = array();
625
	while ($row = $db->fetch_assoc($request))
626
	{
627
		$log_topics[] = array($row['id_member'], $row['id_topic'], $row['id_msg'], $row['unwatched']);
628
629
		// Prevent queries from getting too big. Taking some steam off.
630
		if (count($log_topics) > 500)
631
		{
632
			markTopicsRead($log_topics, true);
633
			$log_topics = array();
634
		}
635
	}
636
	$db->free_result($request);
637
638
	// Now that we have all the topics that *should* be marked read, and by which members...
639
	if (!empty($log_topics))
640
	{
641
		// Insert that information into the database!
642
		markTopicsRead($log_topics, true);
643
	}
644
645
	// Update the number of posts on each board.
646
	$totalTopics = 0;
647
	$totalPosts = 0;
648
	$totalUnapprovedTopics = 0;
649
	$totalUnapprovedPosts = 0;
650
	foreach ($fromBoards as $stats)
651
	{
652
		$db->query('', '
653
			UPDATE {db_prefix}boards
654
			SET
655
				num_posts = CASE WHEN {int:num_posts} > num_posts THEN 0 ELSE num_posts - {int:num_posts} END,
656
				num_topics = CASE WHEN {int:num_topics} > num_topics THEN 0 ELSE num_topics - {int:num_topics} END,
657
				unapproved_posts = CASE WHEN {int:unapproved_posts} > unapproved_posts THEN 0 ELSE unapproved_posts - {int:unapproved_posts} END,
658
				unapproved_topics = CASE WHEN {int:unapproved_topics} > unapproved_topics THEN 0 ELSE unapproved_topics - {int:unapproved_topics} END
659
			WHERE id_board = {int:id_board}',
660
			array(
661
				'id_board' => $stats['id_board'],
662
				'num_posts' => $stats['num_posts'],
663
				'num_topics' => $stats['num_topics'],
664
				'unapproved_posts' => $stats['unapproved_posts'],
665
				'unapproved_topics' => $stats['unapproved_topics'],
666
			)
667
		);
668
		$totalTopics += $stats['num_topics'];
669
		$totalPosts += $stats['num_posts'];
670
		$totalUnapprovedTopics += $stats['unapproved_topics'];
671
		$totalUnapprovedPosts += $stats['unapproved_posts'];
672
	}
673
	$db->query('', '
674
		UPDATE {db_prefix}boards
675
		SET
676
			num_topics = num_topics + {int:total_topics},
677
			num_posts = num_posts + {int:total_posts},' . ($isRecycleDest ? '
678
			unapproved_posts = {int:no_unapproved}, unapproved_topics = {int:no_unapproved}' : '
679
			unapproved_posts = unapproved_posts + {int:total_unapproved_posts},
680
			unapproved_topics = unapproved_topics + {int:total_unapproved_topics}') . '
681
		WHERE id_board = {int:id_board}',
682
		array(
683
			'id_board' => $toBoard,
684
			'total_topics' => $totalTopics,
685
			'total_posts' => $totalPosts,
686
			'total_unapproved_topics' => $totalUnapprovedTopics,
687
			'total_unapproved_posts' => $totalUnapprovedPosts,
688
			'no_unapproved' => 0,
689
		)
690
	);
691
692
	if ($isRecycleDest)
693
	{
694
		$attributes = array(
695
			'id_board' => $toBoard,
696
			'approved' => 1,
697
			'unapproved_posts' => 0,
698
		);
699
	}
700
	else
701
	{
702
		$attributes = array('id_board' => $toBoard);
703
	}
704
705
	// Move the topic.  Done.  :P
706
	setTopicAttribute($topics, $attributes);
707
708
	// If this was going to the recycle bin, check what messages are being recycled, and remove them from the queue.
709
	if ($isRecycleDest && ($totalUnapprovedTopics || $totalUnapprovedPosts))
710
	{
711
		$request = $db->query('', '
712
			SELECT id_msg
713
			FROM {db_prefix}messages
714
			WHERE id_topic IN ({array_int:topics})
715
				and approved = {int:not_approved}',
716
			array(
717
				'topics' => $topics,
718
				'not_approved' => 0,
719
			)
720
		);
721
		$approval_msgs = array();
722
		while ($row = $db->fetch_assoc($request))
723
			$approval_msgs[] = $row['id_msg'];
724
		$db->free_result($request);
725
726
		// Empty the approval queue for these, as we're going to approve them next.
727
		if (!empty($approval_msgs))
728
			$db->query('', '
729
				DELETE FROM {db_prefix}approval_queue
730
				WHERE id_msg IN ({array_int:message_list})
731
					AND id_attach = {int:id_attach}',
732
				array(
733
					'message_list' => $approval_msgs,
734
					'id_attach' => 0,
735
				)
736
			);
737
738
		// Get all the current max and mins.
739
		$topicAttribute = topicAttribute($topics, array('id_topic', 'id_first_msg', 'id_last_msg'));
740
		$topicMaxMin = array();
741
		foreach ($topicAttribute as $row)
742
		{
743
			$topicMaxMin[$row['id_topic']] = array(
744
				'min' => $row['id_first_msg'],
745
				'max' => $row['id_last_msg'],
746
			);
747
		}
748
749
		// Check the MAX and MIN are correct.
750
		$request = $db->query('', '
751
			SELECT id_topic, MIN(id_msg) AS first_msg, MAX(id_msg) AS last_msg
752
			FROM {db_prefix}messages
753
			WHERE id_topic IN ({array_int:topics})
754
			GROUP BY id_topic',
755
			array(
756
				'topics' => $topics,
757
			)
758
		);
759
		while ($row = $db->fetch_assoc($request))
760
		{
761
			// If not, update.
762
			if ($row['first_msg'] != $topicMaxMin[$row['id_topic']]['min'] || $row['last_msg'] != $topicMaxMin[$row['id_topic']]['max'])
763
				setTopicAttribute($row['id_topic'], array(
764
					'id_first_msg' => $row['first_msg'],
765
					'id_last_msg' => $row['last_msg'],
766
				));
767
		}
768
		$db->free_result($request);
769
	}
770
771
	$db->query('', '
772
		UPDATE {db_prefix}messages
773
		SET id_board = {int:id_board}' . ($isRecycleDest ? ',approved = {int:is_approved}' : '') . '
774
		WHERE id_topic IN ({array_int:topics})',
775
		array(
776
			'id_board' => $toBoard,
777
			'topics' => $topics,
778
			'is_approved' => 1,
779
		)
780
	);
781
	$db->query('', '
782
		UPDATE {db_prefix}log_reported
783
		SET id_board = {int:id_board}
784
		WHERE id_topic IN ({array_int:topics})',
785
		array(
786
			'id_board' => $toBoard,
787
			'topics' => $topics,
788
		)
789
	);
790
	$db->query('', '
791
		UPDATE {db_prefix}calendar
792
		SET id_board = {int:id_board}
793
		WHERE id_topic IN ({array_int:topics})',
794
		array(
795
			'id_board' => $toBoard,
796
			'topics' => $topics,
797
		)
798
	);
799
800
	// Mark target board as seen, if it was already marked as seen before.
801
	$request = $db->query('', '
802
		SELECT (COALESCE(lb.id_msg, 0) >= b.id_msg_updated) AS isSeen
803
		FROM {db_prefix}boards AS b
804
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
805
		WHERE b.id_board = {int:id_board}',
806
		array(
807
			'current_member' => $user_info['id'],
808
			'id_board' => $toBoard,
809
		)
810
	);
811
	list ($isSeen) = $db->fetch_row($request);
812
	$db->free_result($request);
813
814
	if (!empty($isSeen) && !$user_info['is_guest'])
815
	{
816
		require_once(SUBSDIR . '/Boards.subs.php');
817
		markBoardsRead($toBoard);
818
	}
819
820
	$cache = Cache::instance();
821
	// Update the cache?
822
	foreach ($topics as $topic_id)
823
		$cache->remove('topic_board-' . $topic_id);
824
825
	require_once(SUBSDIR . '/Post.subs.php');
826
827
	$updates = array_keys($fromBoards);
828
	$updates[] = $toBoard;
829
830
	updateLastMessages(array_unique($updates));
831
832
	// Update 'em pesky stats.
833
	updateTopicStats();
834
	require_once(SUBSDIR . '/Messages.subs.php');
835
	updateMessageStats();
836
	updateSettings(array(
837
		'calendar_updated' => time(),
838
	));
839
840
	if ($log)
841
	{
842
		foreach ($topics as $topic)
843
		{
844
			logAction('move', array('topic' => $topic, 'board_from' => $fromCacheBoards[$topic], 'board_to' => $toBoard));
845
			sendNotifications($topic, 'move');
846
		}
847
	}
848
}
849
850
/**
851
 * Called after a topic is moved to update $board_link and $topic_link to point
852
 * to new location
853
 *
854
 * @param int $move_from The board the topic belongs to
855
 * @param int $id_board The "current" board
856
 * @param int $id_topic The topic id
857
 *
858
 * @return bool
859
 * @throws Elk_Exception topic_already_moved
860
 */
861
function moveTopicConcurrence($move_from, $id_board, $id_topic)
862
{
863
	global $scripturl;
864
865
	$db = database();
866
867
	if (empty($move_from) || empty($id_board) || empty($id_topic))
868
	{
869
		return true;
870
	}
871
872
	if ($move_from == $id_board)
873
	{
874
		return true;
875
	}
876
	else
877
	{
878
		$request = $db->query('', '
879
			SELECT m.subject, b.name
880
			FROM {db_prefix}topics AS t
881
				LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
882
				LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
883
			WHERE t.id_topic = {int:topic_id}
884
			LIMIT 1',
885
			array(
886
				'topic_id' => $id_topic,
887
			)
888
		);
889
		list ($topic_subject, $board_name) = $db->fetch_row($request);
890
		$db->free_result($request);
891
892
		$board_link = '<a href="' . $scripturl . '?board=' . $id_board . '.0">' . $board_name . '</a>';
893
		$topic_link = '<a href="' . $scripturl . '?topic=' . $id_topic . '.0">' . $topic_subject . '</a>';
894
		throw new Elk_Exception('topic_already_moved', false, array($topic_link, $board_link));
895
	}
896
}
897
898
/**
899
 * Determine if the topic has already been deleted by another user.
900
 *
901
 * What it does:
902
 *  - If the topic has been removed and resides in the recycle bin, present confirm dialog
903
 *  - If recycling is not enabled, or user confirms or topic is not in recycle simply returns
904
 */
905
function removeDeleteConcurrence()
906
{
907
	global $modSettings, $board, $scripturl, $context;
908
909
	$recycled_enabled = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']);
910
911
	if ($recycled_enabled && !empty($board))
912
	{
913
		// Trying to removed from the recycle bin
914
		if (!isset($_GET['confirm_delete']) && $modSettings['recycle_board'] == $board)
915
		{
916
			if (isset($_REQUEST['msg']))
917
			{
918
				$confirm_url = $scripturl . '?action=deletemsg;confirm_delete;topic=' . $context['current_topic'] . '.0;msg=' . $_REQUEST['msg'] . ';' . $context['session_var'] . '=' . $context['session_id'];
919
			}
920
			else
921
			{
922
				$confirm_url = $scripturl . '?action=removetopic2;confirm_delete;topic=' . $context['current_topic'] . '.0;' . $context['session_var'] . '=' . $context['session_id'];
923
			}
924
925
			// Give them a prompt before we remove the message
926
			throw new Elk_Exception('post_already_deleted', false, array($confirm_url));
927
		}
928
	}
929
}
930
931
/**
932
 * Increase the number of views of this topic.
933
 *
934
 * @param int $id_topic the topic being viewed or whatnot.
935
 */
936
function increaseViewCounter($id_topic)
937
{
938
	$db = database();
939
940
	$db->query('', '
941
		UPDATE {db_prefix}topics
942
		SET num_views = num_views + 1
943
		WHERE id_topic = {int:current_topic}',
944
		array(
945
			'current_topic' => $id_topic,
946
		)
947
	);
948
}
949
950
/**
951
 * Mark topic(s) as read by the given member, at the specified message.
952
 *
953
 * @param mixed[] $mark_topics array($id_member, $id_topic, $id_msg)
954
 * @param bool $was_set = false - whether the topic has been previously read by the user
955
 */
956
function markTopicsRead($mark_topics, $was_set = false)
957
{
958
	$db = database();
959
960
	if (!is_array($mark_topics))
961
		return;
962
963
	$db->insert($was_set ? 'replace' : 'ignore',
964
		'{db_prefix}log_topics',
965
		array(
966
			'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int',
967
		),
968
		$mark_topics,
969
		array('id_member', 'id_topic')
970
	);
971
}
972
973
/**
974
 * Update user notifications for a topic... or the board it's in.
975
 * @todo look at board notification...
976
 *
977
 * @param int $id_topic
978
 * @param int $id_board
979
 */
980
function updateReadNotificationsFor($id_topic, $id_board)
981
{
982
	global $user_info, $context;
983
984
	$db = database();
985
986
	// Check for notifications on this topic OR board.
987
	$request = $db->query('', '
988
		SELECT sent, id_topic
989
		FROM {db_prefix}log_notify
990
		WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
991
			AND id_member = {int:current_member}
992
		LIMIT 2',
993
		array(
994
			'current_board' => $id_board,
995
			'current_member' => $user_info['id'],
996
			'current_topic' => $id_topic,
997
		)
998
	);
999
1000
	while ($row = $db->fetch_assoc($request))
1001
	{
1002
		// Find if this topic is marked for notification...
1003
		if (!empty($row['id_topic']))
1004
			$context['is_marked_notify'] = true;
1005
1006
		// Only do this once, but mark the notifications as "not sent yet" for next time.
1007
		if (!empty($row['sent']))
1008
		{
1009
			$db->query('', '
1010
				UPDATE {db_prefix}log_notify
1011
				SET sent = {int:is_not_sent}
1012
				WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
1013
					AND id_member = {int:current_member}',
1014
				array(
1015
					'current_board' => $id_board,
1016
					'current_member' => $user_info['id'],
1017
					'current_topic' => $id_topic,
1018
					'is_not_sent' => 0,
1019
				)
1020
			);
1021
			break;
1022
		}
1023
	}
1024
	$db->free_result($request);
1025
}
1026
1027
/**
1028
 * How many topics are still unread since (last visit)
1029
 *
1030
 * @param int $id_board
1031
 * @param int $id_msg_last_visit
1032
 * @return int
1033
 */
1034 View Code Duplication
function getUnreadCountSince($id_board, $id_msg_last_visit)
0 ignored issues
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1035
{
1036
	global $user_info;
1037
1038
	$db = database();
1039
1040
	$request = $db->query('', '
1041
		SELECT COUNT(*)
1042
		FROM {db_prefix}topics AS t
1043
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = {int:current_board} AND lb.id_member = {int:current_member})
1044
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
1045
		WHERE t.id_board = {int:current_board}
1046
			AND t.id_last_msg > COALESCE(lb.id_msg, 0)
1047
			AND t.id_last_msg > COALESCE(lt.id_msg, 0)' .
1048
				(empty($id_msg_last_visit) ? '' : '
1049
			AND t.id_last_msg > {int:id_msg_last_visit}'),
1050
		array(
1051
			'current_board' => $id_board,
1052
			'current_member' => $user_info['id'],
1053
			'id_msg_last_visit' => (int) $id_msg_last_visit,
1054
		)
1055
	);
1056
	list ($unread) = $db->fetch_row($request);
1057
	$db->free_result($request);
1058
1059
	return $unread;
1060
}
1061
1062
/**
1063
 * Returns whether this member has notification turned on for the specified topic.
1064
 *
1065
 * @param int $id_member
1066
 * @param int $id_topic
1067
 * @return bool
1068
 */
1069
function hasTopicNotification($id_member, $id_topic)
1070
{
1071
	$db = database();
1072
1073
	// Find out if they have notification set for this topic already.
1074
	$request = $db->query('', '
1075
		SELECT id_member
1076
		FROM {db_prefix}log_notify
1077
		WHERE id_member = {int:current_member}
1078
			AND id_topic = {int:current_topic}
1079
		LIMIT 1',
1080
		array(
1081
			'current_member' => $id_member,
1082
			'current_topic' => $id_topic,
1083
		)
1084
	);
1085
	$hasNotification = $db->num_rows($request) != 0;
1086
	$db->free_result($request);
1087
1088
	return $hasNotification;
1089
}
1090
1091
/**
1092
 * Set topic notification on or off for the given member.
1093
 *
1094
 * @param int $id_member
1095
 * @param int $id_topic
1096
 * @param bool $on
1097
 */
1098 View Code Duplication
function setTopicNotification($id_member, $id_topic, $on = false)
0 ignored issues
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1099
{
1100
	$db = database();
1101
1102
	if ($on)
1103
	{
1104
		// Attempt to turn notifications on.
1105
		$db->insert('ignore',
1106
			'{db_prefix}log_notify',
1107
			array('id_member' => 'int', 'id_topic' => 'int'),
1108
			array($id_member, $id_topic),
1109
			array('id_member', 'id_topic')
1110
		);
1111
	}
1112
	else
1113
	{
1114
		// Just turn notifications off.
1115
		$db->query('', '
1116
			DELETE FROM {db_prefix}log_notify
1117
			WHERE id_member = {int:current_member}
1118
				AND id_topic = {int:current_topic}',
1119
			array(
1120
				'current_member' => $id_member,
1121
				'current_topic' => $id_topic,
1122
			)
1123
		);
1124
	}
1125
}
1126
1127
/**
1128
 * Get the previous topic from where we are.
1129
 *
1130
 * @param int $id_topic origin topic id
1131
 * @param int $id_board board id
1132
 * @param int $id_member = 0 member id
1133
 * @param bool $includeUnapproved = false whether to include unapproved topics
1134
 * @param bool $includeStickies = true whether to include sticky topics
1135
 * @return int topic number
1136
 */
1137
function previousTopic($id_topic, $id_board, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1138
{
1139
	return topicPointer($id_topic, $id_board, false, $id_member, $includeUnapproved, $includeStickies);
1140
}
1141
1142
/**
1143
 * Get the next topic from where we are.
1144
 *
1145
 * @param int $id_topic origin topic id
1146
 * @param int $id_board board id
1147
 * @param int $id_member = 0 member id
1148
 * @param bool $includeUnapproved = false whether to include unapproved topics
1149
 * @param bool $includeStickies = true whether to include sticky topics
1150
 * @return int topic number
1151
 */
1152
function nextTopic($id_topic, $id_board, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1153
{
1154
	return topicPointer($id_topic, $id_board, true, $id_member, $includeUnapproved, $includeStickies);
1155
}
1156
1157
/**
1158
 * Advance topic pointer.
1159
 * (in either direction)
1160
 * This function is used by previousTopic() and nextTopic()
1161
 * The boolean parameter $next determines direction.
1162
 *
1163
 * @param int $id_topic origin topic id
1164
 * @param int $id_board board id
1165
 * @param bool $next = true whether to increase or decrease the pointer
1166
 * @param int $id_member = 0 member id
1167
 * @param bool $includeUnapproved = false whether to include unapproved topics
1168
 * @param bool $includeStickies = true whether to include sticky topics
1169
 * @return int the topic number
1170
 */
1171
function topicPointer($id_topic, $id_board, $next = true, $id_member = 0, $includeUnapproved = false, $includeStickies = true)
1172
{
1173
	$db = database();
1174
1175
	$request = $db->query('', '
1176
		SELECT t2.id_topic
1177
		FROM {db_prefix}topics AS t
1178
		INNER JOIN {db_prefix}topics AS t2 ON (' .
1179
			(empty($includeStickies) ? '
1180
				t2.id_last_msg {raw:strictly} t.id_last_msg' : '
1181
				(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')
1182
			. ')
1183
		WHERE t.id_topic = {int:current_topic}
1184
			AND t2.id_board = {int:current_board}' .
1185
			($includeUnapproved ? '' : '
1186
				AND (t2.approved = {int:is_approved} OR (t2.id_member_started != {int:id_member_started} AND t2.id_member_started = {int:current_member}))'
1187
				) . '
1188
		ORDER BY' . (
1189
			$includeStickies ? '
1190
				t2.is_sticky {raw:sorting},' :
1191
				'') .
1192
			' t2.id_last_msg {raw:sorting}
1193
		LIMIT 1',
1194
		array(
1195
			'strictly' => $next ? '<' : '>',
1196
			'strictly_equal' => $next ? '<=' : '>=',
1197
			'sorting' => $next ? 'DESC' : '',
1198
			'current_board' => $id_board,
1199
			'current_member' => $id_member,
1200
			'current_topic' => $id_topic,
1201
			'is_approved' => 1,
1202
			'id_member_started' => 0,
1203
		)
1204
	);
1205
1206
	// Was there any?
1207
	if ($db->num_rows($request) == 0)
1208
	{
1209
		$db->free_result($request);
1210
1211
		// Roll over - if we're going prev, get the last - otherwise the first.
1212
		$request = $db->query('', '
1213
			SELECT id_topic
1214
			FROM {db_prefix}topics
1215
			WHERE id_board = {int:current_board}' .
1216
			($includeUnapproved ? '' : '
1217
				AND (approved = {int:is_approved} OR (id_member_started != {int:id_member_started} AND id_member_started = {int:current_member}))') . '
1218
			ORDER BY' . (
1219
				$includeStickies ? ' is_sticky {raw:sorting},' : '') .
1220
				' id_last_msg {raw:sorting}
1221
			LIMIT 1',
1222
			array(
1223
				'sorting' => $next ? 'DESC' : '',
1224
				'current_board' => $id_board,
1225
				'current_member' => $id_member,
1226
				'is_approved' => 1,
1227
				'id_member_started' => 0,
1228
			)
1229
		);
1230
	}
1231
	// Now you can be sure $topic is the id_topic to view.
1232
	list ($topic) = $db->fetch_row($request);
1233
	$db->free_result($request);
1234
1235
	return $topic;
1236
}
1237
1238
/**
1239
 * Set off/on unread reply subscription for a topic
1240
 *
1241
 * @param int $id_member
1242
 * @param int $topic
1243
 * @param bool $on = false
1244
 */
1245
function setTopicWatch($id_member, $topic, $on = false)
1246
{
1247
	global $user_info;
1248
1249
	$db = database();
1250
1251
	// find the current entry if it exists that is
1252
	$was_set = getLoggedTopics($user_info['id'], array($topic));
1253
1254
	// Set topic unwatched on/off for this topic.
1255
	$db->insert(empty($was_set[$topic]) ? 'ignore' : 'replace',
1256
		'{db_prefix}log_topics',
1257
		array('id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'unwatched' => 'int'),
1258
		array($id_member, $topic, !empty($was_set[$topic]['id_msg']) ? $was_set[$topic]['id_msg'] : 0, $on ? 1 : 0),
1259
		array('id_member', 'id_topic')
1260
	);
1261
}
1262
1263
/**
1264
 * Get all the details for a given topic
1265
 * - returns the basic topic information when $full is false
1266
 * - returns topic details, subject, last message read, etc when full is true
1267
 * - uses any integration information (value selects, tables and parameters) if passed and full is true
1268
 *
1269
 * @param mixed[]|int $topic_parameters can also accept a int value for a topic
1270
 * @param string $full defines the values returned by the function:
1271
 *    - if empty returns only the data from {db_prefix}topics
1272
 *    - if 'message' returns also information about the message (subject, body, etc.)
1273
 *    - if 'starter' returns also information about the topic starter (id_member and poster_name)
1274
 *    - if 'all' returns additional infos about the read/unwatched status
1275
 * @param string[] $selects (optional from integration)
1276
 * @param string[] $tables (optional from integration)
1277
 * @return array to topic attributes
1278
 */
1279
function getTopicInfo($topic_parameters, $full = '', $selects = array(), $tables = array())
1280
{
1281
	global $user_info, $modSettings, $board;
1282
1283
	$db = database();
1284
1285
	// Nothing to do
1286
	if (empty($topic_parameters))
1287
		return false;
1288
1289
	// Build what we can with what we were given
1290
	if (!is_array($topic_parameters))
1291
		$topic_parameters = array(
1292
			'topic' => $topic_parameters,
1293
			'member' => $user_info['id'],
1294
			'board' => (int) $board,
1295
		);
1296
1297
	$messages_table = $full === 'message' || $full === 'all' || $full === 'starter';
1298
	$members_table = $full === 'starter';
1299
	$logs_table = $full === 'all';
1300
1301
	// Create the query, taking full and integration in to account
1302
	$request = $db->query('', '
1303
		SELECT
1304
			t.id_topic, t.is_sticky, t.id_board, t.id_first_msg, t.id_last_msg,
1305
			t.id_member_started, t.id_member_updated, t.id_poll,
1306
			t.num_replies, t.num_views, t.num_likes, t.locked, t.redirect_expires,
1307
			t.id_redirect_topic, t.unapproved_posts, t.approved' . ($messages_table ? ',
1308
			ms.subject, ms.body, ms.id_member, ms.poster_time, ms.approved as msg_approved' : '') . ($members_table ? ',
1309
			COALESCE(mem.real_name, ms.poster_name) AS poster_name' : '') . ($logs_table ? ',
1310
			' . ($user_info['is_guest'] ? 't.id_last_msg + 1' : 'COALESCE(lt.id_msg, lmr.id_msg, -1) + 1') . ' AS new_from
1311
			' . (!empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board ? ', t.id_previous_board, t.id_previous_topic' : '') . '
1312
			' . (!$user_info['is_guest'] ? ', COALESCE(lt.unwatched, 0) as unwatched' : '') : '') .
1313
			(!empty($selects) ? ', ' . implode(', ', $selects) : '') . '
1314
		FROM {db_prefix}topics AS t' . ($messages_table ? '
1315
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)' : '') . ($members_table ? '
1316
			LEFT JOIN {db_prefix}members as mem ON (mem.id_member = ms.id_member)' : '') . ($logs_table && !$user_info['is_guest'] ? '
1317
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:topic} AND lt.id_member = {int:member})
1318
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:board} AND lmr.id_member = {int:member})' : '') . (!empty($tables) ? '
1319
			' . implode("\n\t\t\t", $tables) : '') . '
1320
		WHERE t.id_topic = {int:topic}
1321
		LIMIT 1',
1322
			$topic_parameters
1323
	);
1324
	$topic_info = array();
1325
	if ($request !== false)
1326
		$topic_info = $db->fetch_assoc($request);
1327
	$db->free_result($request);
1328
1329
	return $topic_info;
1330
}
1331
1332
/**
1333
 * Get all the details for a given topic and message.
1334
 * Respects permissions and post moderation
1335
 *
1336
 * @param int $topic id of a topic
1337
 * @param int|null $msg the id of a message, if empty, t.id_first_msg is used
1338
 * @return mixed[]|boolean to topic attributes
1339
 */
1340
function getTopicInfoByMsg($topic, $msg = null)
1341
{
1342
	global $user_info, $modSettings;
1343
1344
	// Nothing to do
1345
	if (empty($topic))
1346
		return false;
1347
1348
	$db = database();
1349
1350
	$request = $db->query('', '
1351
		SELECT
1352
			t.locked, t.num_replies, t.id_member_started, t.id_first_msg,
1353
			m.id_msg, m.id_member, m.poster_time, m.subject, m.smileys_enabled, m.body, m.icon,
1354
			m.modified_time, m.modified_name, m.approved
1355
		FROM {db_prefix}messages AS m
1356
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})
1357
		WHERE m.id_msg = {raw:id_msg}
1358
			AND m.id_topic = {int:current_topic}' . (allowedTo('modify_any') || allowedTo('approve_posts') ? '' : (!$modSettings['postmod_active'] ? '
1359
			AND (m.id_member != {int:guest_id} AND m.id_member = {int:current_member})' : '
1360
			AND (m.approved = {int:is_approved} OR (m.id_member != {int:guest_id} AND m.id_member = {int:current_member}))')),
1361
		array(
1362
			'current_member' => $user_info['id'],
1363
			'current_topic' => $topic,
1364
			'id_msg' => empty($msg) ? 't.id_first_msg' : $msg,
1365
			'is_approved' => 1,
1366
			'guest_id' => 0,
1367
		)
1368
	);
1369
	$topic_info = array();
1370
	if ($request !== false)
1371
	{
1372
		$topic_info = $db->fetch_assoc($request);
1373
	}
1374
	$db->free_result($request);
1375
1376
	return $topic_info;
1377
}
1378
1379
/**
1380
 * So long as you are sure... all old posts will be gone.
1381
 * Used in Maintenance.controller.php to prune old topics.
1382
 *
1383
 * @param int[] $boards
1384
 * @param string $delete_type
1385
 * @param boolean $exclude_stickies
1386
 * @param int $older_than
1387
 * @throws Elk_Exception
1388
 */
1389
function removeOldTopics(array $boards, $delete_type, $exclude_stickies, $older_than)
1390
{
1391
	$db = database();
1392
1393
	// Custom conditions.
1394
	$condition = '';
1395
	$condition_params = array(
1396
		'boards' => $boards,
1397
		'poster_time' => $older_than,
1398
	);
1399
1400
	// Just moved notice topics?
1401
	if ($delete_type == 'moved')
1402
	{
1403
		$condition .= '
1404
			AND m.icon = {string:icon}
1405
			AND t.locked = {int:locked}';
1406
		$condition_params['icon'] = 'moved';
1407
		$condition_params['locked'] = 1;
1408
	}
1409
	// Otherwise, maybe locked topics only?
1410
	elseif ($delete_type == 'locked')
1411
	{
1412
		$condition .= '
1413
			AND t.locked = {int:locked}';
1414
		$condition_params['locked'] = 1;
1415
	}
1416
1417
	// Exclude stickies?
1418
	if ($exclude_stickies)
1419
	{
1420
		$condition .= '
1421
			AND t.is_sticky = {int:is_sticky}';
1422
		$condition_params['is_sticky'] = 0;
1423
	}
1424
1425
	// All we're gonna do here is grab the id_topic's and send them to removeTopics().
1426
	$request = $db->query('', '
1427
		SELECT t.id_topic
1428
		FROM {db_prefix}topics AS t
1429
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
1430
		WHERE
1431
			m.poster_time < {int:poster_time}' . $condition . '
1432
			AND t.id_board IN ({array_int:boards})',
1433
		$condition_params
1434
	);
1435
	$topics = array();
1436
	while ($row = $db->fetch_assoc($request))
1437
		$topics[] = $row['id_topic'];
1438
	$db->free_result($request);
1439
1440
	removeTopics($topics, false, true);
1441
}
1442
1443
/**
1444
 * Retrieve all topics started by the given member.
1445
 *
1446
 * @param int $memberID
1447
 */
1448
function topicsStartedBy($memberID)
1449
{
1450
	$db = database();
1451
1452
	// Fetch all topics started by this user.
1453
	$request = $db->query('', '
1454
		SELECT t.id_topic
1455
		FROM {db_prefix}topics AS t
1456
		WHERE t.id_member_started = {int:selected_member}',
1457
			array(
1458
				'selected_member' => $memberID,
1459
			)
1460
		);
1461
	$topicIDs = array();
1462
	while ($row = $db->fetch_assoc($request))
1463
		$topicIDs[] = $row['id_topic'];
1464
	$db->free_result($request);
1465
1466
	return $topicIDs;
1467
}
1468
1469
/**
1470
 * Retrieve the messages of the given topic, that are at or after
1471
 * a message.
1472
 * Used by split topics actions.
1473
 *
1474
 * @param int $id_topic
1475
 * @param int $id_msg
1476
 * @param bool $include_current = false
1477
 * @param bool $only_approved = false
1478
 *
1479
 * @return array message ids
1480
 */
1481
function messagesSince($id_topic, $id_msg, $include_current = false, $only_approved = false)
1482
{
1483
	$db = database();
1484
1485
	// Fetch the message IDs of the topic that are at or after the message.
1486
	$request = $db->query('', '
1487
		SELECT id_msg
1488
		FROM {db_prefix}messages
1489
		WHERE id_topic = {int:current_topic}
1490
			AND id_msg ' . ($include_current ? '>=' : '>') . ' {int:last_msg}' . ($only_approved ? '
1491
			AND approved = {int:approved}' : ''),
1492
		array(
1493
			'current_topic' => $id_topic,
1494
			'last_msg' => $id_msg,
1495
			'approved' => 1,
1496
		)
1497
	);
1498
	$messages = array();
1499
	while ($row = $db->fetch_assoc($request))
1500
		$messages[] = $row['id_msg'];
1501
	$db->free_result($request);
1502
1503
	return $messages;
1504
}
1505
1506
/**
1507
 * This function returns the number of messages in a topic,
1508
 * posted after $id_msg.
1509
 *
1510
 * @param int $id_topic
1511
 * @param int $id_msg
1512
 * @param bool $include_current = false
1513
 * @param bool $only_approved = false
1514
 *
1515
 * @return int
1516
 */
1517
function countMessagesSince($id_topic, $id_msg, $include_current = false, $only_approved = false)
1518
{
1519
	$db = database();
1520
1521
	// Give us something to work with
1522
	if (empty($id_topic) || empty($id_msg))
1523
		return false;
1524
1525
	$request = $db->query('', '
1526
		SELECT COUNT(*)
1527
		FROM {db_prefix}messages
1528
		WHERE id_topic = {int:current_topic}
1529
			AND id_msg ' . ($include_current ? '>=' : '>') . ' {int:last_msg}' . ($only_approved ? '
1530
			AND approved = {int:approved}' : '') . '
1531
		LIMIT 1',
1532
		array(
1533
			'current_topic' => $id_topic,
1534
			'last_msg' => $id_msg,
1535
			'approved' => 1,
1536
		)
1537
	);
1538
	list ($count) = $db->fetch_row($request);
1539
	$db->free_result($request);
1540
1541
	return $count;
1542
}
1543
1544
/**
1545
 * Returns how many messages are in a topic before the specified message id.
1546
 * Used in display to compute the start value for a specific message.
1547
 *
1548
 * @param int $id_topic
1549
 * @param int $id_msg
1550
 * @param bool $include_current = false
1551
 * @param bool $only_approved = false
1552
 * @param bool $include_own = false
1553
 * @return int
1554
 */
1555
function countMessagesBefore($id_topic, $id_msg, $include_current = false, $only_approved = false, $include_own = false)
1556
{
1557
	global $user_info;
1558
1559
	$db = database();
1560
1561
	$request = $db->query('', '
1562
		SELECT COUNT(*)
1563
		FROM {db_prefix}messages
1564
		WHERE id_msg ' . ($include_current ? '<=' : '<') . ' {int:id_msg}
1565
			AND id_topic = {int:current_topic}' . ($only_approved ? '
1566
			AND (approved = {int:is_approved}' . ($include_own ? '
1567
			OR id_member = {int:current_member}' : '') . ')' : ''),
1568
		array(
1569
			'current_member' => $user_info['id'],
1570
			'current_topic' => $id_topic,
1571
			'id_msg' => $id_msg,
1572
			'is_approved' => 1,
1573
		)
1574
	);
1575
	list ($count) = $db->fetch_row($request);
1576
	$db->free_result($request);
1577
1578
	return $count;
1579
}
1580
1581
/**
1582
 * Select a part of the messages in a topic.
1583
 *
1584
 * @param int $topic
1585
 * @param int $start The item to start with (for pagination purposes)
1586
 * @param int $items_per_page  The number of items to show per page
1587
 * @param mixed[] $messages
1588
 * @param bool $only_approved
1589
 */
1590
function selectMessages($topic, $start, $items_per_page, $messages = array(), $only_approved = false)
1591
{
1592
	$db = database();
1593
1594
	// Get the messages and stick them into an array.
1595
	$request = $db->query('', '
1596
		SELECT m.subject, COALESCE(mem.real_name, m.poster_name) AS real_name, m.poster_time, m.body, m.id_msg, m.smileys_enabled, m.id_member
1597
		FROM {db_prefix}messages AS m
1598
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1599
		WHERE m.id_topic = {int:current_topic}' . (empty($messages['before']) ? '' : '
1600
			AND m.id_msg < {int:msg_before}') . (empty($messages['after']) ? '' : '
1601
			AND m.id_msg > {int:msg_after}') . (empty($messages['excluded']) ? '' : '
1602
			AND m.id_msg NOT IN ({array_int:no_split_msgs})') . (empty($messages['included']) ? '' : '
1603
			AND m.id_msg IN ({array_int:split_msgs})') . (!$only_approved ? '' : '
1604
			AND approved = {int:is_approved}') . '
1605
		ORDER BY m.id_msg DESC
1606
		LIMIT {int:start}, {int:messages_per_page}',
1607
		array(
1608
			'current_topic' => $topic,
1609
			'no_split_msgs' => !empty($messages['excluded']) ? $messages['excluded'] : array(),
1610
			'split_msgs' => !empty($messages['included']) ? $messages['included'] : array(),
1611
			'is_approved' => 1,
1612
			'start' => $start,
1613
			'messages_per_page' => $items_per_page,
1614
			'msg_before' => !empty($messages['before']) ? (int) $messages['before'] : 0,
1615
			'msg_after' => !empty($messages['after']) ? (int) $messages['after'] : 0,
1616
		)
1617
	);
1618
1619
	$messages = array();
1620
	$parser = \BBC\ParserWrapper::instance();
1621
1622
	for ($counter = 0; $row = $db->fetch_assoc($request); $counter++)
1623
	{
1624
		$row['subject'] = censor($row['subject']);
1625
		$row['body'] = censor($row['body']);
1626
1627
		$row['body'] = $parser->parseMessage($row['body'], (bool) $row['smileys_enabled']);
1628
1629
		$messages[$row['id_msg']] = array(
1630
			'id' => $row['id_msg'],
1631
			'alternate' => $counter % 2,
1632
			'subject' => $row['subject'],
1633
			'time' => standardTime($row['poster_time']),
1634
			'html_time' => htmlTime($row['poster_time']),
1635
			'timestamp' => forum_time(true, $row['poster_time']),
1636
			'body' => $row['body'],
1637
			'poster' => $row['real_name'],
1638
			'id_poster' => $row['id_member'],
1639
		);
1640
	}
1641
	$db->free_result($request);
1642
1643
	return $messages;
1644
}
1645
1646
/**
1647
 * Loads all the messages of a topic
1648
 * Used when printing or other functions that require a topic listing
1649
 *
1650
 * @param int $topic
1651
 * @param string $render defaults to print style rendering for parse_bbc
1652
 */
1653
function topicMessages($topic, $render = 'print')
1654
{
1655
	global $modSettings, $user_info;
1656
1657
	$db = database();
1658
1659
	$request = $db->query('', '
1660
		SELECT subject, poster_time, body, COALESCE(mem.real_name, poster_name) AS poster_name, id_msg
1661
		FROM {db_prefix}messages AS m
1662
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1663
		WHERE m.id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && !allowedTo('approve_posts') ? '
1664
			AND (m.approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR m.id_member = {int:current_member}') . ')' : '') . '
1665
		ORDER BY m.id_msg',
1666
		array(
1667
			'current_topic' => $topic,
1668
			'is_approved' => 1,
1669
			'current_member' => $user_info['id'],
1670
		)
1671
	);
1672
1673
	$posts = array();
1674
	$parser = \BBC\ParserWrapper::instance();
1675
1676
	if ($render === 'print')
1677
	{
1678
		$parser->getCodes()->setForPrinting();
1679
	}
1680
1681
	while ($row = $db->fetch_assoc($request))
1682
	{
1683
		// Censor the subject and message.
1684
		$row['subject'] = censor($row['subject']);
1685
		$row['body'] = censor($row['body']);
1686
1687
		$posts[$row['id_msg']] = array(
1688
			'subject' => $row['subject'],
1689
			'member' => $row['poster_name'],
1690
			'time' => standardTime($row['poster_time'], false),
1691
			'html_time' => htmlTime($row['poster_time']),
1692
			'timestamp' => forum_time(true, $row['poster_time']),
1693
			'body' => $parser->parseMessage($row['body'], $render !== 'print'),
1694
			'id_msg' => $row['id_msg'],
1695
		);
1696
	}
1697
	$db->free_result($request);
1698
1699
	return $posts;
1700
}
1701
1702
/**
1703
 * Load message image attachments for use in the print page function
1704
 * Returns array of file attachment name along with width/height properties
1705
 * Will only return approved attachments
1706
 *
1707
 * @param int[] $id_messages
1708
 */
1709
function messagesAttachments($id_messages)
1710
{
1711
	global $modSettings;
1712
1713
	require_once(SUBSDIR . '/Attachments.subs.php');
1714
1715
	$db = database();
1716
1717
	$request = $db->query('', '
1718
		SELECT
1719
			a.id_attach, a.id_msg, a.approved, a.width, a.height, a.file_hash, a.filename, a.id_folder, a.mime_type
1720
		FROM {db_prefix}attachments AS a
1721
		WHERE a.id_msg IN ({array_int:message_list})
1722
			AND a.attachment_type = {int:attachment_type}',
1723
		array(
1724
			'message_list' => $id_messages,
1725
			'attachment_type' => 0,
1726
			'is_approved' => 1,
1727
		)
1728
	);
1729
	$temp = array();
1730
	$printattach = array();
1731
	while ($row = $db->fetch_assoc($request))
1732
	{
1733
		$temp[$row['id_attach']] = $row;
1734
		if (!isset($printattach[$row['id_msg']]))
1735
			$printattach[$row['id_msg']] = array();
1736
	}
1737
	$db->free_result($request);
1738
	ksort($temp);
1739
1740
	// Load them into $context so the template can use them
1741
	foreach ($temp as $row)
1742
	{
1743
		if (!empty($row['width']) && !empty($row['height']))
1744
		{
1745
			if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $row['height'] * ($modSettings['max_image_width'] / $row['width']) <= $modSettings['max_image_height']))
1746
			{
1747 View Code Duplication
				if ($row['width'] > $modSettings['max_image_width'])
1748
				{
1749
					$row['height'] = floor($row['height'] * ($modSettings['max_image_width'] / $row['width']));
1750
					$row['width'] = $modSettings['max_image_width'];
1751
				}
1752
			}
1753 View Code Duplication
			elseif (!empty($modSettings['max_image_width']))
1754
			{
1755
				if ($row['height'] > $modSettings['max_image_height'])
1756
				{
1757
					$row['width'] = floor($row['width'] * $modSettings['max_image_height'] / $row['height']);
1758
					$row['height'] = $modSettings['max_image_height'];
1759
				}
1760
			}
1761
1762
			$row['filename'] = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
1763
1764
			// save for the template
1765
			$printattach[$row['id_msg']][] = $row;
1766
		}
1767
	}
1768
1769
	return $printattach;
1770
}
1771
1772
/**
1773
 * Retrieve unapproved posts of the member
1774
 * in a specific topic
1775
 *
1776
 * @param int $id_topic topic id
1777
 * @param int $id_member member id
1778
 * @return array|int empty array if no member supplied, otherwise number of posts
1779
 */
1780
function unapprovedPosts($id_topic, $id_member)
1781
{
1782
	$db = database();
1783
1784
	// not all guests are the same!
1785
	if (empty($id_member))
1786
		return array();
1787
1788
	$request = $db->query('', '
1789
			SELECT COUNT(id_member) AS my_unapproved_posts
1790
			FROM {db_prefix}messages
1791
			WHERE id_topic = {int:current_topic}
1792
				AND id_member = {int:current_member}
1793
				AND approved = 0',
1794
			array(
1795
				'current_topic' => $id_topic,
1796
				'current_member' => $id_member,
1797
			)
1798
		);
1799
	list ($myUnapprovedPosts) = $db->fetch_row($request);
1800
	$db->free_result($request);
1801
1802
	return $myUnapprovedPosts;
1803
}
1804
1805
/**
1806
 * Update topic info after a successful split of a topic.
1807
 *
1808
 * @param mixed[] $options
1809
 * @param int $id_board
1810
 */
1811
function updateSplitTopics($options, $id_board)
1812
{
1813
	$db = database();
1814
1815
	// Any associated reported posts better follow...
1816
	$db->query('', '
1817
		UPDATE {db_prefix}log_reported
1818
		SET id_topic = {int:id_topic}
1819
		WHERE id_msg IN ({array_int:split_msgs})
1820
			AND type = {string:a_message}',
1821
		array(
1822
			'split_msgs' => $options['splitMessages'],
1823
			'id_topic' => $options['split2_ID_TOPIC'],
1824
			'a_message' => 'msg',
1825
		)
1826
	);
1827
1828
	// Mess with the old topic's first, last, and number of messages.
1829
	setTopicAttribute($options['split1_ID_TOPIC'], array(
1830
		'num_replies' => $options['split1_replies'],
1831
		'id_first_msg' => $options['split1_first_msg'],
1832
		'id_last_msg' => $options['split1_last_msg'],
1833
		'id_member_started' => $options['split1_firstMem'],
1834
		'id_member_updated' => $options['split1_lastMem'],
1835
		'unapproved_posts' => $options['split1_unapprovedposts'],
1836
	));
1837
1838
	// Now, put the first/last message back to what they should be.
1839
	setTopicAttribute($options['split2_ID_TOPIC'], array(
1840
		'id_first_msg' => $options['split2_first_msg'],
1841
		'id_last_msg' => $options['split2_last_msg'],
1842
	));
1843
1844
	// If the new topic isn't approved ensure the first message flags
1845
	// this just in case.
1846
	if (!$options['split2_approved'])
1847
		$db->query('', '
1848
			UPDATE {db_prefix}messages
1849
			SET approved = {int:approved}
1850
			WHERE id_msg = {int:id_msg}
1851
				AND id_topic = {int:id_topic}',
1852
			array(
1853
				'approved' => 0,
1854
				'id_msg' => $options['split2_first_msg'],
1855
				'id_topic' => $options['split2_ID_TOPIC'],
1856
			)
1857
		);
1858
1859
	// The board has more topics now (Or more unapproved ones!).
1860
	$db->query('', '
1861
		UPDATE {db_prefix}boards
1862
		SET ' . ($options['split2_approved'] ? '
1863
			num_topics = num_topics + 1' : '
1864
			unapproved_topics = unapproved_topics + 1') . '
1865
		WHERE id_board = {int:id_board}',
1866
		array(
1867
			'id_board' => $id_board,
1868
		)
1869
	);
1870
}
1871
1872
/**
1873
 * Find out who started a topic, and the lock status
1874
 *
1875
 * @param int $topic
1876
 * @return array with id_member_started and locked
1877
 */
1878
function topicStatus($topic)
1879
{
1880
	// Find out who started the topic, and the lock status.
1881
	$starter = topicAttribute($topic, array('id_member_started', 'locked'));
1882
1883
	return array($starter['id_member_started'], $starter['locked']);
1884
}
1885
1886
/**
1887
 * Set attributes for a topic, i.e. locked, sticky.
1888
 * Parameter $attributes is an array where the key is the column name of the
1889
 * attribute to change, and the value is... the new value of the attribute.
1890
 * It sets the new value for the attribute as passed to it.
1891
 * <b>It is currently limited to integer values only</b>
1892
 *
1893
 * @param int|int[] $topic
1894
 * @param mixed[] $attributes
1895
 * @todo limited to integer attributes
1896
 * @return int number of row affected
1897
 */
1898
function setTopicAttribute($topic, $attributes)
1899
{
1900
	$db = database();
1901
1902
	$update = array();
1903
	foreach ($attributes as $key => $attr)
1904
	{
1905
		$attributes[$key] = (int) $attr;
1906 24
		$update[] = '
1907
				' . $key . ' = {int:' . $key . '}';
1908 24
	}
1909 24
1910
	if (empty($update))
1911
		return false;
1912 24
1913 16
	$attributes['current_topic'] = (array) $topic;
1914
1915
	$db->query('', '
1916
		UPDATE {db_prefix}topics
1917
		SET ' . implode(',', $update) . '
1918 24
		WHERE id_topic IN ({array_int:current_topic})',
1919 24
		$attributes
1920 24
	);
1921 16
1922
	return $db->affected_rows();
1923 24
}
1924 16
1925
/**
1926 24
 * Retrieve the locked or sticky status of a topic.
1927
 *
1928 24
 * @param int|int[] $id_topic topic to get the status for
1929
 * @param string|string[] $attributes Basically the column names
1930 24
 * @return array named array based on attributes requested
1931 16
 */
1932 8
function topicAttribute($id_topic, $attributes)
1933 16
{
1934
	$db = database();
1935 24
1936
	// @todo maybe add a filer for known attributes... or not
1937
// 	$attributes = array(
1938
// 		'locked' => 'locked',
1939
// 		'sticky' => 'is_sticky',
1940
// 	);
1941
1942
	// check the lock status
1943
	$request = $db->query('', '
1944
		SELECT {raw:attribute}
1945
		FROM {db_prefix}topics
1946
		WHERE id_topic IN ({array_int:current_topic})',
1947 36
		array(
1948
			'current_topic' => (array) $id_topic,
1949
			'attribute' => implode(',', (array) $attributes),
1950
		)
1951
	);
1952
1953
	if (is_array($id_topic))
1954
	{
1955
		$status = array();
1956 36
		while ($row = $db->fetch_assoc($request))
1957
			$status[] = $row;
1958
	}
1959 24
	else
1960
	{
1961 36
		$status = $db->fetch_assoc($request);
1962 36
	}
1963
	$db->free_result($request);
1964 24
1965
	return $status;
1966 36
}
1967 24
1968 36
/**
1969 36
 * Retrieve some topic attributes based on the user:
1970 36
 *   - locked
1971 24
 *   - notify
1972
 *   - is_sticky
1973
 *   - id_poll
1974 18
 *   - id_last_msg
1975
 *   - id_member of the first message in the topic
1976 36
 *   - id_first_msg
1977
 *   - subject of the first message in the topic
1978 36
 *   - last_post_time that is poster_time if poster_time > modified_time, or
1979
 *       modified_time otherwise
1980
 *
1981
 * @param int $id_topic topic to get the status for
1982
 * @param int $user a user id
1983
 * @return mixed[]
1984
 */
1985
function topicUserAttributes($id_topic, $user)
1986
{
1987
	$db = database();
1988
1989
	$request = $db->query('', '
1990
		SELECT
1991
			t.locked, COALESCE(ln.id_topic, 0) AS notify, t.is_sticky, t.id_poll,
1992
			t.id_last_msg, mf.id_member, t.id_first_msg, mf.subject,
1993
			CASE WHEN ml.poster_time > ml.modified_time THEN ml.poster_time ELSE ml.modified_time END AS last_post_time
1994
		FROM {db_prefix}topics AS t
1995
			LEFT JOIN {db_prefix}log_notify AS ln ON (ln.id_topic = t.id_topic AND ln.id_member = {int:current_member})
1996
			LEFT JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1997
			LEFT JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1998
		WHERE t.id_topic = {int:current_topic}
1999
		LIMIT 1',
2000
		array(
2001
			'current_member' => $user,
2002
			'current_topic' => $id_topic,
2003
		)
2004
	);
2005
	$return = $db->fetch_assoc($request);
2006
	$db->free_result($request);
2007
2008
	return $return;
2009
}
2010
2011
/**
2012
 * Retrieve some details about the topic
2013
 *
2014
 * @param int[] $topics an array of topic id
2015
 */
2016
function topicsDetails($topics)
2017
{
2018
	$returns = topicAttribute($topics, array('id_topic', 'id_member_started', 'id_board', 'locked', 'approved', 'unapproved_posts'));
2019
2020
	return $returns;
2021
}
2022
2023
/**
2024
 * Toggle sticky status for the passed topics and logs the action.
2025
 *
2026
 * @param int[] $topics
2027
 * @param bool $log If true the action is logged
2028
 * @return int Number of topics toggled
2029
 * @throws Elk_Exception
2030
 */
2031
function toggleTopicSticky($topics, $log = false)
2032
{
2033
	$db = database();
2034
2035
	$topics = is_array($topics) ? $topics : array($topics);
2036
2037
	$db->query('', '
2038
		UPDATE {db_prefix}topics
2039
		SET is_sticky = CASE WHEN is_sticky = 1 THEN 0 ELSE 1 END
2040
		WHERE id_topic IN ({array_int:sticky_topic_ids})',
2041
		array(
2042
			'sticky_topic_ids' => $topics,
2043
		)
2044
	);
2045
2046
	$toggled = $db->affected_rows();
2047
2048
	if ($log)
2049
	{
2050
		// Get the board IDs and Sticky status
2051
		$topicAttributes = topicAttribute($topics, array('id_topic', 'id_board', 'is_sticky'));
2052
		$stickyCacheBoards = array();
2053
		$stickyCacheStatus = array();
2054
		foreach ($topicAttributes as $row)
2055
		{
2056
			$stickyCacheBoards[$row['id_topic']] = $row['id_board'];
2057
			$stickyCacheStatus[$row['id_topic']] = empty($row['is_sticky']);
2058
		}
2059
2060
		foreach ($topics as $topic)
2061
		{
2062
			logAction($stickyCacheStatus[$topic] ? 'unsticky' : 'sticky', array('topic' => $topic, 'board' => $stickyCacheBoards[$topic]));
2063
			sendNotifications($topic, 'sticky');
2064
		}
2065
	}
2066
2067
	return $toggled;
2068
}
2069
2070
/**
2071
 * Get topics from the log_topics table belonging to a certain user
2072
 *
2073
 * @param int $member a member id
2074
 * @param int[] $topics an array of topics
2075
 * @return array an array of topics in the table (key) and its unwatched status (value)
2076
 *
2077
 * @todo find a better name
2078
 */
2079 View Code Duplication
function getLoggedTopics($member, $topics)
0 ignored issues
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2080
{
2081
	$db = database();
2082
2083
	$request = $db->query('', '
2084
		SELECT id_topic, id_msg, unwatched
2085
		FROM {db_prefix}log_topics
2086
		WHERE id_topic IN ({array_int:selected_topics})
2087
			AND id_member = {int:current_user}',
2088
		array(
2089
			'selected_topics' => $topics,
2090
			'current_user' => $member,
2091
		)
2092
	);
2093
	$logged_topics = array();
2094
	while ($row = $db->fetch_assoc($request))
2095
		$logged_topics[$row['id_topic']] = $row;
2096
	$db->free_result($request);
2097
2098
	return $logged_topics;
2099
}
2100
2101
/**
2102
 * Returns a list of topics ids and their subjects
2103
 *
2104
 * @param int[] $topic_ids
2105
 */
2106
function topicsList($topic_ids)
2107
{
2108
	global $modSettings;
2109
2110
	// you have to want *something* from this function
2111
	if (empty($topic_ids))
2112
		return array();
2113
2114
	$db = database();
2115
2116
	$topics = array();
2117
2118
	$result = $db->query('', '
2119
		SELECT t.id_topic, m.subject
2120
		FROM {db_prefix}topics AS t
2121
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
2122
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
2123
		WHERE {query_see_board}
2124
			AND t.id_topic IN ({array_int:topic_list})' . ($modSettings['postmod_active'] ? '
2125
			AND t.approved = {int:is_approved}' : '') . '
2126
		LIMIT {int:limit}',
2127
		array(
2128
			'topic_list' => $topic_ids,
2129
			'is_approved' => 1,
2130
			'limit' => count($topic_ids),
2131
		)
2132
	);
2133
	while ($row = $db->fetch_assoc($result))
2134
	{
2135
		$topics[$row['id_topic']] = array(
2136
			'id_topic' => $row['id_topic'],
2137
			'subject' => censor($row['subject']),
2138
		);
2139
	}
2140
	$db->free_result($result);
2141
2142
	return $topics;
2143
}
2144
2145
/**
2146
 * Get each post and poster in this topic and take care of user settings such as
2147
 * limit or sort direction.
2148
 *
2149
 * @param int $topic
2150
 * @param mixed[] $limit
2151
 * @param boolean $sort set to false for a desc sort
2152
 * @return array
2153
 */
2154
function getTopicsPostsAndPoster($topic, $limit, $sort)
2155
{
2156
	global $modSettings, $user_info;
2157
2158
	$db = database();
2159
2160
	$topic_details = array(
2161
		'messages' => array(),
2162
		'all_posters' => array(),
2163
	);
2164
2165
	$request = $db->query('display_get_post_poster', '
2166
		SELECT id_msg, id_member, approved
2167
		FROM {db_prefix}messages
2168
		WHERE id_topic = {int:current_topic}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : '
2169
		GROUP BY id_msg
2170
		HAVING (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')') . '
2171
		ORDER BY id_msg ' . ($sort ? '' : 'DESC') . ($limit['messages_per_page'] == -1 ? '' : '
2172
		LIMIT ' . $limit['start'] . ', ' . $limit['offset']),
2173
		array(
2174
			'current_member' => $user_info['id'],
2175
			'current_topic' => $topic,
2176
			'is_approved' => 1,
2177
			'blank_id_member' => 0,
2178
		)
2179
	);
2180
	while ($row = $db->fetch_assoc($request))
2181
	{
2182
		if (!empty($row['id_member']))
2183
			$topic_details['all_posters'][$row['id_msg']] = $row['id_member'];
2184
			$topic_details['messages'][] = $row['id_msg'];
2185
	}
2186
	$db->free_result($request);
2187
2188
	return $topic_details;
2189
}
2190
2191
/**
2192
 * Remove a batch of messages (or topics)
2193
 *
2194
 * @param int[] $messages
2195
 * @param mixed[] $messageDetails
2196
 * @param string $type = replies
2197
 * @throws Elk_Exception
2198
 */
2199
function removeMessages($messages, $messageDetails, $type = 'replies')
2200
{
2201
	global $modSettings;
2202
2203
	// @todo something's not right, removeMessage() does check permissions,
2204
	// removeTopics() doesn't
2205
	if ($type == 'topics')
2206
	{
2207
		removeTopics($messages);
2208
2209
		// and tell the world about it
2210
		foreach ($messages as $topic)
2211
		{
2212
			// Note, only log topic ID in native form if it's not gone forever.
2213
			logAction('remove', array(
2214
				(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']));
2215
		}
2216
	}
2217
	else
2218
	{
2219
		$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
2220
		foreach ($messages as $post)
2221
		{
2222
			$remover->removeMessage($post);
2223
		}
2224
	}
2225
}
2226
2227
/**
2228
 * Approve a batch of posts (or topics in their own right)
2229
 *
2230
 * @param int[] $messages
2231
 * @param mixed[] $messageDetails
2232
 * @param string $type = replies
2233
 * @throws Elk_Exception
2234
 */
2235
function approveMessages($messages, $messageDetails, $type = 'replies')
2236
{
2237
	if ($type == 'topics')
2238
	{
2239
		approveTopics($messages, true, true);
2240
	}
2241
	else
2242
	{
2243
		require_once(SUBSDIR . '/Post.subs.php');
2244
		approvePosts($messages);
2245
2246
		// and tell the world about it again
2247
		foreach ($messages as $post)
2248
			logAction('approve', array('topic' => $messageDetails[$post]['topic'], 'subject' => $messageDetails[$post]['subject'], 'member' => $messageDetails[$post]['member'], 'board' => $messageDetails[$post]['board']));
2249
	}
2250
}
2251
2252
/**
2253
 * Approve topics, all we got.
2254
 *
2255
 * @param int[] $topics array of topics ids
2256
 * @param bool $approve = true
2257
 * @param bool $log if true logs the action.
2258
 * @throws Elk_Exception
2259
 */
2260
function approveTopics($topics, $approve = true, $log = false)
2261
{
2262
	global $board;
2263
2264
	if (!is_array($topics))
2265
		$topics = array($topics);
2266
2267
	if (empty($topics))
2268
		return false;
2269
2270
	$db = database();
2271
2272
	$approve_type = $approve ? 0 : 1;
2273
2274
	if ($log)
2275
	{
2276
		$log_action = $approve ? 'approve_topic' : 'unapprove_topic';
2277
2278
		// We need unapproved topic ids, their authors and the subjects!
2279
		$request = $db->query('', '
2280
			SELECT t.id_topic, t.id_member_started, m.subject
2281
			FROM {db_prefix}topics as t
2282
				LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
2283
			WHERE t.id_topic IN ({array_int:approve_topic_ids})
2284
				AND t.approved = {int:approve_type}
2285
			LIMIT ' . count($topics),
2286
			array(
2287
				'approve_topic_ids' => $topics,
2288
				'approve_type' => $approve_type,
2289
			)
2290
		);
2291
		while ($row = $db->fetch_assoc($request))
2292
		{
2293
			logAction($log_action, array('topic' => $row['id_topic'], 'subject' => $row['subject'], 'member' => $row['id_member_started'], 'board' => $board));
2294
		}
2295
		$db->free_result($request);
2296
	}
2297
2298
	// Just get the messages to be approved and pass through...
2299
	$request = $db->query('', '
2300
		SELECT id_msg
2301
		FROM {db_prefix}messages
2302
		WHERE id_topic IN ({array_int:topic_list})
2303
			AND approved = {int:approve_type}',
2304
		array(
2305
			'topic_list' => $topics,
2306
			'approve_type' => $approve_type,
2307
		)
2308
	);
2309
	$msgs = array();
2310
	while ($row = $db->fetch_assoc($request))
2311
		$msgs[] = $row['id_msg'];
2312
	$db->free_result($request);
2313
2314
	require_once(SUBSDIR . '/Post.subs.php');
2315
	return approvePosts($msgs, $approve);
2316
}
2317
2318
/**
2319
 * Post a message at the end of the original topic
2320
 *
2321
 * @param string $reason the text that will become the message body
2322
 * @param string $subject the text that will become the message subject
2323
 * @param mixed[] $board_info some board information (at least id, name, if posts are counted)
2324
 * @param string $new_topic used to build the url for moving to a new topic
2325
 * @throws Elk_Exception
2326
 */
2327
function postSplitRedirect($reason, $subject, $board_info, $new_topic)
2328
{
2329
	global $scripturl, $user_info, $language, $txt, $topic, $board;
2330
2331
	// Should be in the boardwide language.
2332
	if ($user_info['language'] != $language)
2333
		theme()->getTemplates()->loadLanguageFile('index', $language);
2334
2335
	preparsecode($reason);
2336
2337
	// Add a URL onto the message.
2338
	$reason = strtr($reason, array(
2339
		$txt['movetopic_auto_board'] => '[url=' . $scripturl . '?board=' . $board_info['id'] . '.0]' . $board_info['name'] . '[/url]',
2340
		$txt['movetopic_auto_topic'] => '[iurl]' . $scripturl . '?topic=' . $new_topic . '.0[/iurl]'
2341
	));
2342
2343
	$msgOptions = array(
2344
		'subject' => $txt['split'] . ': ' . strtr(Util::htmltrim(Util::htmlspecialchars($subject)), array("\r" => '', "\n" => '', "\t" => '')),
2345
		'body' => $reason,
2346
		'icon' => 'moved',
2347
		'smileys_enabled' => 1,
2348
	);
2349
2350
	$topicOptions = array(
2351
		'id' => $topic,
2352
		'board' => $board,
2353
		'mark_as_read' => true,
2354
	);
2355
2356
	$posterOptions = array(
2357
		'id' => $user_info['id'],
2358
		'update_post_count' => empty($board_info['count_posts']),
2359
	);
2360
2361
	createPost($msgOptions, $topicOptions, $posterOptions);
2362
}
2363
2364
/**
2365
 * General function to split off a topic.
2366
 * creates a new topic and moves the messages with the IDs in
2367
 * array messagesToBeSplit to the new topic.
2368
 * the subject of the newly created topic is set to 'newSubject'.
2369
 * marks the newly created message as read for the user splitting it.
2370
 * updates the statistics to reflect a newly created topic.
2371
 * logs the action in the moderation log.
2372
 * a notification is sent to all users monitoring this topic.
2373
 *
2374
 * @param int    $split1_ID_TOPIC
2375
 * @param int[]  $splitMessages
2376
 * @param string $new_subject
2377
 *
2378
 * @return int the topic ID of the new split topic.
2379
 * @throws Elk_Exception no_posts_selected
2380
 */
2381
function splitTopic($split1_ID_TOPIC, $splitMessages, $new_subject)
2382
{
2383
	global $txt;
2384
2385
	$db = database();
2386
2387
	// Nothing to split?
2388
	if (empty($splitMessages))
2389
		throw new Elk_Exception('no_posts_selected', false);
2390
2391
	// Get some board info.
2392
	$topicAttribute = topicAttribute($split1_ID_TOPIC, array('id_board', 'approved'));
2393
	$id_board = $topicAttribute['id_board'];
2394
	$split1_approved = $topicAttribute['approved'];
2395
2396
	// Find the new first and last not in the list. (old topic)
2397
	$request = $db->query('', '
2398
		SELECT
2399
			MIN(m.id_msg) AS myid_first_msg, MAX(m.id_msg) AS myid_last_msg, COUNT(*) AS message_count, m.approved
2400
		FROM {db_prefix}messages AS m
2401
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:id_topic})
2402
		WHERE m.id_msg NOT IN ({array_int:no_msg_list})
2403
			AND m.id_topic = {int:id_topic}
2404
		GROUP BY m.approved
2405
		ORDER BY m.approved DESC
2406
		LIMIT 2',
2407
		array(
2408
			'id_topic' => $split1_ID_TOPIC,
2409
			'no_msg_list' => $splitMessages,
2410
		)
2411
	);
2412
	// You can't select ALL the messages!
2413
	if ($db->num_rows($request) == 0)
2414
		throw new Elk_Exception('selected_all_posts', false);
2415
2416
	$split1_first_msg = null;
2417
	$split1_last_msg = null;
2418
2419
	while ($row = $db->fetch_assoc($request))
2420
	{
2421
		// Get the right first and last message dependant on approved state...
2422
		if (empty($split1_first_msg) || $row['myid_first_msg'] < $split1_first_msg)
2423
			$split1_first_msg = $row['myid_first_msg'];
2424
2425
		if (empty($split1_last_msg) || $row['approved'])
2426
			$split1_last_msg = $row['myid_last_msg'];
2427
2428
		// Get the counts correct...
2429
		if ($row['approved'])
2430
		{
2431
			$split1_replies = $row['message_count'] - 1;
2432
			$split1_unapprovedposts = 0;
2433
		}
2434
		else
2435
		{
2436
			if (!isset($split1_replies))
2437
				$split1_replies = 0;
2438
			// If the topic isn't approved then num replies must go up by one... as first post wouldn't be counted.
2439
			elseif (!$split1_approved)
2440
				$split1_replies++;
2441
2442
			$split1_unapprovedposts = $row['message_count'];
2443
		}
2444
	}
2445
	$db->free_result($request);
2446
	$split1_firstMem = getMsgMemberID($split1_first_msg);
2447
	$split1_lastMem = getMsgMemberID($split1_last_msg);
2448
2449
	// Find the first and last in the list. (new topic)
2450
	$request = $db->query('', '
2451
		SELECT MIN(id_msg) AS myid_first_msg, MAX(id_msg) AS myid_last_msg, COUNT(*) AS message_count, approved
2452
		FROM {db_prefix}messages
2453
		WHERE id_msg IN ({array_int:msg_list})
2454
			AND id_topic = {int:id_topic}
2455
		GROUP BY id_topic, approved
2456
		ORDER BY approved DESC
2457
		LIMIT 2',
2458
		array(
2459
			'msg_list' => $splitMessages,
2460
			'id_topic' => $split1_ID_TOPIC,
2461
		)
2462
	);
2463
	while ($row = $db->fetch_assoc($request))
2464
	{
2465
		// As before get the right first and last message dependant on approved state...
2466
		if (empty($split2_first_msg) || $row['myid_first_msg'] < $split2_first_msg)
2467
			$split2_first_msg = $row['myid_first_msg'];
2468
2469
		if (empty($split2_last_msg) || $row['approved'])
2470
			$split2_last_msg = $row['myid_last_msg'];
2471
2472
		// Then do the counts again...
2473
		if ($row['approved'])
2474
		{
2475
			$split2_approved = true;
2476
			$split2_replies = $row['message_count'] - 1;
2477
			$split2_unapprovedposts = 0;
2478
		}
2479
		else
2480
		{
2481
			// Should this one be approved??
2482
			if ($split2_first_msg == $row['myid_first_msg'])
2483
				$split2_approved = false;
2484
2485
			if (!isset($split2_replies))
2486
				$split2_replies = 0;
2487
			// As before, fix number of replies.
2488
			elseif (!$split2_approved)
0 ignored issues
show
Bug introduced by
The variable $split2_approved does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2489
				$split2_replies++;
2490
2491
			$split2_unapprovedposts = $row['message_count'];
2492
		}
2493
	}
2494
	$db->free_result($request);
2495
	$split2_firstMem = getMsgMemberID($split2_first_msg);
0 ignored issues
show
Bug introduced by
The variable $split2_first_msg does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2496
	$split2_lastMem = getMsgMemberID($split2_last_msg);
0 ignored issues
show
Bug introduced by
The variable $split2_last_msg does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2497
2498
	// No database changes yet, so let's double check to see if everything makes at least a little sense.
2499
	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
Bug introduced by
The variable $split1_replies does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $split2_replies does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $split1_unapprovedposts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $split2_unapprovedposts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2500
		throw new Elk_Exception('cant_find_messages');
2501
2502
	// You cannot split off the first message of a topic.
2503
	if ($split1_first_msg > $split2_first_msg)
2504
		throw new Elk_Exception('split_first_post', false);
2505
2506
	// The message that is starting the new topic may have likes, these become topic likes
2507
	require_once(SUBSDIR . '/Likes.subs.php');
2508
	$split2_first_msg_likes = messageLikeCount($split2_first_msg);
2509
2510
	// We're off to insert the new topic!  Use 0 for now to avoid UNIQUE errors.
2511
	$db->insert('',
2512
		'{db_prefix}topics',
2513
		array(
2514
			'id_board' => 'int',
2515
			'id_member_started' => 'int',
2516
			'id_member_updated' => 'int',
2517
			'id_first_msg' => 'int',
2518
			'id_last_msg' => 'int',
2519
			'num_replies' => 'int',
2520
			'unapproved_posts' => 'int',
2521
			'approved' => 'int',
2522
			'is_sticky' => 'int',
2523
			'num_likes' => 'int',
2524
		),
2525
		array(
2526
			(int) $id_board, $split2_firstMem, $split2_lastMem, 0,
2527
			0, $split2_replies, $split2_unapprovedposts, (int) $split2_approved, 0, $split2_first_msg_likes,
2528
		),
2529
		array('id_topic')
2530
	);
2531
	$split2_ID_TOPIC = $db->insert_id('{db_prefix}topics', 'id_topic');
2532
	if ($split2_ID_TOPIC <= 0)
2533
		throw new Elk_Exception('cant_insert_topic');
2534
2535
	// Move the messages over to the other topic.
2536
	$new_subject = strtr(Util::htmltrim(Util::htmlspecialchars($new_subject)), array("\r" => '', "\n" => '', "\t" => ''));
2537
2538
	// Check the subject length.
2539
	if (Util::strlen($new_subject) > 100)
2540
		$new_subject = Util::substr($new_subject, 0, 100);
2541
2542
	// Valid subject?
2543
	if ($new_subject != '')
2544
	{
2545
		$db->query('', '
2546
			UPDATE {db_prefix}messages
2547
			SET
2548
				id_topic = {int:id_topic},
2549
				subject = CASE WHEN id_msg = {int:split_first_msg} THEN {string:new_subject} ELSE {string:new_subject_replies} END
2550
			WHERE id_msg IN ({array_int:split_msgs})',
2551
			array(
2552
				'split_msgs' => $splitMessages,
2553
				'id_topic' => $split2_ID_TOPIC,
2554
				'new_subject' => $new_subject,
2555
				'split_first_msg' => $split2_first_msg,
2556
				'new_subject_replies' => $txt['response_prefix'] . $new_subject,
2557
			)
2558
		);
2559
2560
		// Cache the new topics subject... we can do it now as all the subjects are the same!
2561
		require_once(SUBSDIR . '/Messages.subs.php');
2562
		updateSubjectStats($split2_ID_TOPIC, $new_subject);
2563
	}
2564
2565
	// Any associated reported posts better follow...
2566
	require_once(SUBSDIR . '/Topic.subs.php');
2567
	updateSplitTopics(array(
2568
		'splitMessages' => $splitMessages,
2569
		'split1_replies' => $split1_replies,
2570
		'split1_first_msg' => $split1_first_msg,
2571
		'split1_last_msg' => $split1_last_msg,
2572
		'split1_firstMem' => $split1_firstMem,
2573
		'split1_lastMem' => $split1_lastMem,
2574
		'split1_unapprovedposts' => $split1_unapprovedposts,
2575
		'split1_ID_TOPIC' => $split1_ID_TOPIC,
2576
		'split2_first_msg' => $split2_first_msg,
2577
		'split2_last_msg' => $split2_last_msg,
2578
		'split2_ID_TOPIC' => $split2_ID_TOPIC,
2579
		'split2_approved' => $split2_approved,
2580
	), $id_board);
2581
2582
	require_once(SUBSDIR . '/FollowUps.subs.php');
2583
2584
	// Let's see if we can create a stronger bridge between the two topics
2585
	// @todo not sure what message from the oldest topic I should link to the new one, so I'll go with the first
2586
	linkMessages($split1_first_msg, $split2_ID_TOPIC);
2587
2588
	// Copy log topic entries.
2589
	// @todo This should really be chunked.
2590
	$request = $db->query('', '
2591
		SELECT id_member, id_msg, unwatched
2592
		FROM {db_prefix}log_topics
2593
		WHERE id_topic = {int:id_topic}',
2594
		array(
2595
			'id_topic' => (int) $split1_ID_TOPIC,
2596
		)
2597
	);
2598
	if ($db->num_rows($request) > 0)
2599
	{
2600
		$replaceEntries = array();
2601 View Code Duplication
		while ($row = $db->fetch_assoc($request))
2602
			$replaceEntries[] = array($row['id_member'], $split2_ID_TOPIC, $row['id_msg'], $row['unwatched']);
2603
2604
		require_once(SUBSDIR . '/Topic.subs.php');
2605
		markTopicsRead($replaceEntries, false);
2606
		unset($replaceEntries);
2607
	}
2608
	$db->free_result($request);
2609
2610
	// Housekeeping.
2611
	updateTopicStats();
2612
	updateLastMessages($id_board);
2613
2614
	logAction('split', array('topic' => $split1_ID_TOPIC, 'new_topic' => $split2_ID_TOPIC, 'board' => $id_board));
2615
2616
	// Notify people that this topic has been split?
2617
	require_once(SUBSDIR . '/Notification.subs.php');
2618
	sendNotifications($split1_ID_TOPIC, 'split');
2619
2620
	// If there's a search index that needs updating, update it...
2621
	$search = new \ElkArte\Search\Search;
2622
	$searchAPI = $search->findSearchAPI();
2623
	if (is_callable(array($searchAPI, 'topicSplit')))
2624
		$searchAPI->topicSplit($split2_ID_TOPIC, $splitMessages);
2625
2626
	// Return the ID of the newly created topic.
2627
	return $split2_ID_TOPIC;
2628
}
2629
2630
/**
2631
 * If we are also moving the topic somewhere else, let's try do to it
2632
 * Includes checks for permissions move_own/any, etc.
2633
 *
2634
 * @param mixed[] $boards an array containing basic info of the origin and destination boards (from splitDestinationBoard)
2635
 * @param int $totopic id of the destination topic
2636
 * @throws Elk_Exception
2637
 */
2638
function splitAttemptMove($boards, $totopic)
2639
{
2640
	global $board, $user_info;
2641
2642
	$db = database();
2643
2644
	// If the starting and final boards are different we have to check some permissions and stuff
2645
	if ($boards['destination']['id'] != $board)
2646
	{
2647
		$doMove = false;
2648
		if (allowedTo('move_any'))
2649
			$doMove = true;
2650
		else
2651
		{
2652
			$new_topic = getTopicInfo($totopic);
2653
			if ($new_topic['id_member_started'] == $user_info['id'] && allowedTo('move_own'))
2654
				$doMove = true;
2655
		}
2656
2657
		if ($doMove)
2658
		{
2659
			// Update member statistics if needed
2660
			// @todo this should probably go into a function...
2661
			if ($boards['destination']['count_posts'] != $boards['current']['count_posts'])
2662
			{
2663
				$request = $db->query('', '
2664
					SELECT id_member
2665
					FROM {db_prefix}messages
2666
					WHERE id_topic = {int:current_topic}
2667
						AND approved = {int:is_approved}',
2668
					array(
2669
						'current_topic' => $totopic,
2670
						'is_approved' => 1,
2671
					)
2672
				);
2673
				$posters = array();
2674
				while ($row = $db->fetch_assoc($request))
2675
				{
2676
					if (!isset($posters[$row['id_member']]))
2677
						$posters[$row['id_member']] = 0;
2678
2679
					$posters[$row['id_member']]++;
2680
				}
2681
				$db->free_result($request);
2682
2683
				require_once(SUBSDIR . '/Members.subs.php');
2684 View Code Duplication
				foreach ($posters as $id_member => $posts)
2685
				{
2686
					// The board we're moving from counted posts, but not to.
2687
					if (empty($boards['current']['count_posts']))
2688
						updateMemberData($id_member, array('posts' => 'posts - ' . $posts));
2689
					// The reverse: from didn't, to did.
2690
					else
2691
						updateMemberData($id_member, array('posts' => 'posts + ' . $posts));
2692
				}
2693
			}
2694
2695
			// And finally move it!
2696
			moveTopics($totopic, $boards['destination']['id']);
2697
		}
2698
		else
2699
			$boards['destination'] = $boards['current'];
2700
	}
2701
}
2702
2703
/**
2704
 * Retrieves information of the current and destination board of a split topic
2705
 *
2706
 * @param int $toboard
2707
 *
2708
 * @return array
2709
 * @throws Elk_Exception no_board
2710
 */
2711
function splitDestinationBoard($toboard = 0)
2712
{
2713
	global $board, $topic;
2714
2715
	$current_board = boardInfo($board, $topic);
2716
	if (empty($current_board))
2717
		throw new Elk_Exception('no_board');
2718
2719
	if (!empty($toboard) && $board !== $toboard)
2720
	{
2721
		$destination_board = boardInfo($toboard);
2722
		if (empty($destination_board))
2723
			throw new Elk_Exception('no_board');
2724
	}
2725
2726
	if (!isset($destination_board))
2727
		$destination_board = array_merge($current_board, array('id' => $board));
2728
	else
2729
		$destination_board['id'] = $toboard;
2730
2731
	return array('current' => $current_board, 'destination' => $destination_board);
2732
}
2733
2734
/**
2735
 * Retrieve topic notifications count.
2736
 * (used by createList() callbacks, amongst others.)
2737
 *
2738
 * @param int $memID id_member
2739
 * @return integer
2740
 */
2741
function topicNotificationCount($memID)
2742
{
2743
	global $user_info, $modSettings;
2744
2745
	$db = database();
2746
2747
	$request = $db->query('', '
2748
		SELECT COUNT(*)
2749
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2750
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2751
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2752
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2753
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2754
			AND t.approved = {int:is_approved}' : ''),
2755
		array(
2756
			'selected_member' => $memID,
2757
			'is_approved' => 1,
2758
		)
2759
	);
2760
	list ($totalNotifications) = $db->fetch_row($request);
2761
	$db->free_result($request);
2762
2763
	return (int) $totalNotifications;
2764
}
2765
2766
/**
2767
 * Retrieve all topic notifications for the given user.
2768
 * (used by createList() callbacks)
2769
 *
2770
 * @param int $start The item to start with (for pagination purposes)
2771
 * @param int $items_per_page  The number of items to show per page
2772
 * @param string $sort A string indicating how to sort the results
2773
 * @param int $memID id_member
2774
 * @return array
2775
 */
2776
function topicNotifications($start, $items_per_page, $sort, $memID)
2777
{
2778
	global $scripturl, $user_info, $modSettings;
2779
2780
	$db = database();
2781
2782
	// All the topics with notification on...
2783
	$request = $db->query('', '
2784
		SELECT
2785
			COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from, b.id_board, b.name,
2786
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2787
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2788
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name
2789
		FROM {db_prefix}log_notify AS ln
2790
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2791
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2792
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2793
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2794
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2795
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2796
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2797
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2798
		WHERE ln.id_member = {int:selected_member}
2799
		ORDER BY {raw:sort}
2800
		LIMIT {int:offset}, {int:items_per_page}',
2801
		array(
2802
			'current_member' => $user_info['id'],
2803
			'is_approved' => 1,
2804
			'selected_member' => $memID,
2805
			'sort' => $sort,
2806
			'offset' => $start,
2807
			'items_per_page' => $items_per_page,
2808
		)
2809
	);
2810
	$notification_topics = array();
2811
	while ($row = $db->fetch_assoc($request))
2812
	{
2813
		$row['subject'] = censor($row['subject']);
2814
2815
		$notification_topics[] = array(
2816
			'id' => $row['id_topic'],
2817
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2818
			'poster_updated_link' => empty($row['id_member_updated']) ? $row['last_real_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member_updated'] . '">' . $row['last_real_name'] . '</a>',
2819
			'subject' => $row['subject'],
2820
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2821
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2822
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2823
			'new_from' => $row['new_from'],
2824
			'updated' => standardTime($row['poster_time']),
2825
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2826
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2827
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2828
		);
2829
	}
2830
	$db->free_result($request);
2831
2832
	return $notification_topics;
2833
}
2834
2835
/**
2836
 * Get a list of posters in this topic, and their posts counts in the topic.
2837
 * Used to update users posts counts when topics are moved or are deleted.
2838
 *
2839
 * @param int $id_topic topic id to work with
2840
 */
2841
function postersCount($id_topic)
2842
{
2843
	$db = database();
2844
2845
	// We only care about approved topics, the rest don't count.
2846
	$request = $db->query('', '
2847
		SELECT id_member
2848
		FROM {db_prefix}messages
2849
		WHERE id_topic = {int:current_topic}
2850
			AND approved = {int:is_approved}',
2851
		array(
2852
			'current_topic' => $id_topic,
2853
			'is_approved' => 1,
2854
		)
2855
	);
2856
	$posters = array();
2857
	while ($row = $db->fetch_assoc($request))
2858
	{
2859
		if (!isset($posters[$row['id_member']]))
2860
			$posters[$row['id_member']] = 0;
2861
2862
		$posters[$row['id_member']]++;
2863
	}
2864
	$db->free_result($request);
2865
2866
	return $posters;
2867
}
2868
2869
/**
2870
 * Counts topics from the given id_board.
2871
 *
2872
 * @param int $board
2873
 * @param bool $approved
2874
 * @return int
2875
 */
2876
function countTopicsByBoard($board, $approved = false)
2877
{
2878
	$db = database();
2879
2880
	// How many topics are on this board?  (used for paging.)
2881
	$request = $db->query('', '
2882
		SELECT COUNT(*)
2883
		FROM {db_prefix}topics AS t
2884
		WHERE t.id_board = {int:id_board}' . (empty($approved) ? '
2885
			AND t.approved = {int:is_approved}' : ''),
2886
		array(
2887
			'id_board' => $board,
2888
			'is_approved' => 1,
2889
		)
2890
	);
2891
	list ($topics) = $db->fetch_row($request);
2892
	$db->free_result($request);
2893
2894
	return $topics;
2895
}
2896
2897
/**
2898
 * Determines topics which can be merged from a specific board.
2899
 *
2900
 * @param int $id_board
2901
 * @param int $id_topic
2902
 * @param bool $approved
2903
 * @param int $offset
2904
 * @return array
2905
 */
2906
function mergeableTopics($id_board, $id_topic, $approved, $offset)
2907
{
2908
	global $modSettings, $scripturl;
2909
2910
	$db = database();
2911
2912
	// Get some topics to merge it with.
2913
	$request = $db->query('', '
2914
		SELECT t.id_topic, m.subject, m.id_member, COALESCE(mem.real_name, m.poster_name) AS poster_name
2915
		FROM {db_prefix}topics AS t
2916
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
2917
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
2918
		WHERE t.id_board = {int:id_board}
2919
			AND t.id_topic != {int:id_topic}' . (empty($approved) ? '
2920
			AND t.approved = {int:is_approved}' : '') . '
2921
		ORDER BY t.is_sticky DESC, t.id_last_msg DESC
2922
		LIMIT {int:offset}, {int:limit}',
2923
		array(
2924
			'id_board' => $id_board,
2925
			'id_topic' => $id_topic,
2926
			'offset' => $offset,
2927
			'limit' => $modSettings['defaultMaxTopics'],
2928
			'is_approved' => 1,
2929
		)
2930
	);
2931
	$topics = array();
2932
	while ($row = $db->fetch_assoc($request))
2933
	{
2934
		$row['subject'] = censor($row['subject']);
2935
2936
		$topics[] = array(
2937
			'id' => $row['id_topic'],
2938
			'poster' => array(
2939
				'id' => $row['id_member'],
2940
				'name' => $row['poster_name'],
2941
				'href' => empty($row['id_member']) ? '' : $scripturl . '?action=profile;u=' . $row['id_member'],
2942
				'link' => empty($row['id_member']) ? $row['poster_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '" target="_blank" class="new_win">' . $row['poster_name'] . '</a>'
2943
			),
2944
			'subject' => $row['subject'],
2945
			'js_subject' => addcslashes(addslashes($row['subject']), '/')
2946
		);
2947
	}
2948
	$db->free_result($request);
2949
2950
	return $topics;
2951
}
2952
2953
/**
2954
 * Determines all messages from a given array of topics.
2955
 *
2956
 * @param int[] $topics integer array of topics to work with
2957
 * @return array
2958
 */
2959
function messagesInTopics($topics)
2960
{
2961
	$db = database();
2962
2963
	// Obtain all the message ids we are going to affect.
2964
	$request = $db->query('', '
2965
		SELECT id_msg
2966
		FROM {db_prefix}messages
2967
		WHERE id_topic IN ({array_int:topic_list})',
2968
		array(
2969
			'topic_list' => $topics,
2970
	));
2971
	$messages = array();
2972
	while ($row = $db->fetch_assoc($request))
2973
		$messages[] = $row['id_msg'];
2974 36
	$db->free_result($request);
2975
2976
	return $messages;
2977 36
}
2978
2979
/**
2980 24
 * Retrieves the members that posted in a group of topics.
2981
 *
2982 36
 * @param int[] $topics integer array of topics to work with
2983 24
 * @return array of topics each member posted in (grouped by members)
2984 36
 */
2985 36
function topicsPosters($topics)
2986 36
{
2987 36
	$db = database();
2988
2989 36
	// Obtain all the member ids
2990
	$members = array();
2991
	$request = $db->query('', '
2992
		SELECT id_member, id_topic
2993
		FROM {db_prefix}messages
2994
		WHERE id_topic IN ({array_int:topic_list})',
2995
		array(
2996
			'topic_list' => $topics,
2997
	));
2998
	while ($row = $db->fetch_assoc($request))
2999
		$members[$row['id_member']][] = $row['id_topic'];
3000
	$db->free_result($request);
3001
3002
	return $members;
3003
}
3004
3005
/**
3006
 * Updates all the tables involved when two or more topics are merged
3007
 *
3008
 * @param int $first_msg the first message of the new topic
3009
 * @param int[] $topics ids of all the topics merged
3010
 * @param int $id_topic id of the merged topic
3011
 * @param int $target_board id of the target board where the topic will resides
3012
 * @param string $target_subject subject of the new topic
3013
 * @param string $enforce_subject if not empty all the messages will be set to the same subject
3014
 * @param int[] $notifications array of topics with active notifications
3015
 */
3016
function fixMergedTopics($first_msg, $topics, $id_topic, $target_board, $target_subject, $enforce_subject, $notifications)
3017
{
3018
	$db = database();
3019
3020
	// Delete the remaining topics.
3021
	$deleted_topics = array_diff($topics, array($id_topic));
3022
	$db->query('', '
3023
		DELETE FROM {db_prefix}topics
3024
		WHERE id_topic IN ({array_int:deleted_topics})',
3025
		array(
3026
			'deleted_topics' => $deleted_topics,
3027
		)
3028
	);
3029
3030
	$db->query('', '
3031
		DELETE FROM {db_prefix}log_search_subjects
3032
		WHERE id_topic IN ({array_int:deleted_topics})',
3033
		array(
3034
			'deleted_topics' => $deleted_topics,
3035
		)
3036
	);
3037
3038
	// Change the topic IDs of all messages that will be merged.  Also adjust subjects if 'enforce subject' was checked.
3039
	$db->query('', '
3040
		UPDATE {db_prefix}messages
3041
		SET
3042
			id_topic = {int:id_topic},
3043
			id_board = {int:target_board}' . (empty($enforce_subject) ? '' : ',
3044
			subject = {string:subject}') . '
3045
		WHERE id_topic IN ({array_int:topic_list})',
3046
		array(
3047
			'topic_list' => $topics,
3048
			'id_topic' => $id_topic,
3049
			'target_board' => $target_board,
3050
			'subject' => response_prefix() . $target_subject,
3051
		)
3052
	);
3053
3054
	// Any reported posts should reflect the new board.
3055
	$db->query('', '
3056
		UPDATE {db_prefix}log_reported
3057
		SET
3058
			id_topic = {int:id_topic},
3059
			id_board = {int:target_board}
3060
		WHERE id_topic IN ({array_int:topics_list})',
3061
		array(
3062
			'topics_list' => $topics,
3063
			'id_topic' => $id_topic,
3064
			'target_board' => $target_board,
3065
		)
3066
	);
3067
3068
	// Change the subject of the first message...
3069
	$db->query('', '
3070
		UPDATE {db_prefix}messages
3071
		SET subject = {string:target_subject}
3072
		WHERE id_msg = {int:first_msg}',
3073
		array(
3074
			'first_msg' => $first_msg,
3075
			'target_subject' => $target_subject,
3076
		)
3077
	);
3078
3079
	// Adjust all calendar events to point to the new topic.
3080
	$db->query('', '
3081
		UPDATE {db_prefix}calendar
3082
		SET
3083
			id_topic = {int:id_topic},
3084
			id_board = {int:target_board}
3085
		WHERE id_topic IN ({array_int:deleted_topics})',
3086
		array(
3087
			'deleted_topics' => $deleted_topics,
3088
			'id_topic' => $id_topic,
3089
			'target_board' => $target_board,
3090
		)
3091
	);
3092
3093
	// Merge log topic entries.
3094
	// The unwatched setting comes from the oldest topic
3095
	$request = $db->query('', '
3096
		SELECT id_member, MIN(id_msg) AS new_id_msg, unwatched
3097
		FROM {db_prefix}log_topics
3098
		WHERE id_topic IN ({array_int:topics})
3099
		GROUP BY id_member',
3100
		array(
3101
			'topics' => $topics,
3102
		)
3103
	);
3104
3105
	if ($db->num_rows($request) > 0)
3106
	{
3107
		$replaceEntries = array();
3108 View Code Duplication
		while ($row = $db->fetch_assoc($request))
3109
			$replaceEntries[] = array($row['id_member'], $id_topic, $row['new_id_msg'], $row['unwatched']);
3110
3111
		markTopicsRead($replaceEntries, true);
3112
		unset($replaceEntries);
3113
3114
		// Get rid of the old log entries.
3115
		$db->query('', '
3116
			DELETE FROM {db_prefix}log_topics
3117
			WHERE id_topic IN ({array_int:deleted_topics})',
3118
			array(
3119
				'deleted_topics' => $deleted_topics,
3120
			)
3121
		);
3122
	}
3123
	$db->free_result($request);
3124
3125
	if (!empty($notifications))
3126
	{
3127
		$request = $db->query('', '
3128
			SELECT id_member, MAX(sent) AS sent
3129
			FROM {db_prefix}log_notify
3130
			WHERE id_topic IN ({array_int:topics_list})
3131
			GROUP BY id_member',
3132
			array(
3133
				'topics_list' => $notifications,
3134
			)
3135
		);
3136
		if ($db->num_rows($request) > 0)
3137
		{
3138
			$replaceEntries = array();
3139 View Code Duplication
			while ($row = $db->fetch_assoc($request))
3140
				$replaceEntries[] = array($row['id_member'], $id_topic, 0, $row['sent']);
3141
3142
			$db->insert('replace',
3143
					'{db_prefix}log_notify',
3144
					array('id_member' => 'int', 'id_topic' => 'int', 'id_board' => 'int', 'sent' => 'int'),
3145
					$replaceEntries,
3146
					array('id_member', 'id_topic', 'id_board')
3147
				);
3148
			unset($replaceEntries);
3149
3150
			$db->query('', '
3151
				DELETE FROM {db_prefix}log_topics
3152
				WHERE id_topic IN ({array_int:deleted_topics})',
3153
				array(
3154
					'deleted_topics' => $deleted_topics,
3155
				)
3156
			);
3157
		}
3158
		$db->free_result($request);
3159
	}
3160
}
3161
3162
/**
3163
 * Load the subject from a given topic id.
3164
 *
3165
 * @param int $id_topic
3166
 *
3167
 * @return string
3168
 * @throws Elk_Exception topic_gone
3169
 */
3170
function getSubject($id_topic)
3171
{
3172
	global $modSettings;
3173
3174
	$db = database();
3175
3176
	$request = $db->query('', '
3177
		SELECT ms.subject
3178
		FROM {db_prefix}topics AS t
3179
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
3180
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
3181
		WHERE t.id_topic = {int:search_topic_id}
3182
			AND {query_see_board}' . ($modSettings['postmod_active'] ? '
3183
			AND t.approved = {int:is_approved_true}' : '') . '
3184
		LIMIT 1',
3185
		array(
3186
			'is_approved_true' => 1,
3187
			'search_topic_id' => $id_topic,
3188
		)
3189
	);
3190
3191
	if ($db->num_rows($request) == 0)
3192
		throw new Elk_Exception('topic_gone', false);
3193
3194
	list ($subject) = $db->fetch_row($request);
3195
	$db->free_result($request);
3196
3197
	return $subject;
3198
}
3199
3200
/**
3201
 * This function updates the total number of topics,
3202
 * or if parameter $increment is true it simply increments them.
3203
 *
3204
 * @param bool|null $increment = null if true, increment + 1 the total topics, otherwise recount all topics
3205
 */
3206
function updateTopicStats($increment = null)
3207
{
3208
	global $modSettings;
3209
3210
	$db = database();
3211
3212
	if ($increment === true)
3213
		updateSettings(array('totalTopics' => true), true);
3214
	else
3215
	{
3216
		// Get the number of topics - a SUM is better for InnoDB tables.
3217
		// We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
3218
		$request = $db->query('', '
3219
			SELECT SUM(num_topics + unapproved_topics) AS total_topics
3220
			FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
3221 60
			WHERE id_board != {int:recycle_board}' : ''),
3222
			array(
3223 60
				'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
3224
			)
3225 60
		);
3226 60
		$row = $db->fetch_assoc($request);
3227
		$db->free_result($request);
3228
3229
		updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics']));
3230
	}
3231 36
}
3232
3233 36
/**
3234 36
 * Toggles the locked status of the passed id_topic's checking for permissions.
3235
 *
3236 36
 * @param int[] $topics The topics to lock (can be an id or an array of ids).
3237
 * @param bool $log if true logs the action.
3238 24
 * @throws Elk_Exception
3239 36
 */
3240 36
function toggleTopicsLock($topics, $log = false)
3241
{
3242 36
	global $board, $user_info;
3243
3244 60
	$db = database();
3245
3246
	$needs_check = !empty($board) && !allowedTo('lock_any');
3247
	$lockCache = array();
3248
3249
	$topicAttribute = topicAttribute($topics, array('id_topic', 'locked', 'id_board', 'id_member_started'));
3250
3251
	foreach ($topicAttribute as $row)
3252
	{
3253
		// Skip the entry if it needs to be checked and the user is not the owen and
3254
		// the topic was not locked or locked by someone with more permissions
3255
		if ($needs_check && ($user_info['id'] != $row['id_member_started'] || !in_array($row['locked'], array(0, 2))))
3256
			continue;
3257
3258
		$lockCache[] = $row['id_topic'];
3259
3260
		if ($log)
3261
		{
3262
			$lockStatus = empty($row['locked']) ? 'lock' : 'unlock';
3263
3264
			logAction($lockStatus, array('topic' => $row['id_topic'], 'board' => $row['id_board']));
3265
			sendNotifications($row['id_topic'], $lockStatus);
3266
		}
3267
	}
3268
3269
	// It could just be that *none* were their own topics...
3270
	if (!empty($lockCache))
3271
	{
3272
		// Alternate the locked value.
3273
		$db->query('', '
3274
			UPDATE {db_prefix}topics
3275
			SET locked = CASE WHEN locked = {int:is_locked} THEN ' . (allowedTo('lock_any') ? '1' : '2') . ' ELSE 0 END
3276
			WHERE id_topic IN ({array_int:locked_topic_ids})',
3277
			array(
3278
				'locked_topic_ids' => $lockCache,
3279
				'is_locked' => 0,
3280
			)
3281
		);
3282
	}
3283
}
3284