Completed
Pull Request — development (#3620)
by Emanuele
07:38 queued 07:38
created

Display::loadLikeFunction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.9765

Importance

Changes 0
Metric Value
cc 2
eloc 12
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 28
ccs 3
cts 8
cp 0.375
crap 2.9765
rs 9.8666
1
<?php
2
3
/**
4
 * This controls topic display, with all related functions, its is the forum
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Controller;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Exceptions\Exception;
21
use ElkArte\MembersList;
22
use ElkArte\MessagesCallback\BodyParser\Normal;
23
use ElkArte\MessagesCallback\DisplayRenderer;
24
use ElkArte\MessagesDelete;
25
use ElkArte\MessageTopicIcons;
26
use ElkArte\Languages\Txt;
27
use ElkArte\User;
28
use ElkArte\ValuesContainer;
29
30
/**
31
 * This controller is the most important and probably most accessed of all.
32
 * It controls topic display, with all related.
33
 */
34
class Display extends AbstractController
35
{
36
	/** @var null|object The template layers object */
37
	protected $_template_layers;
38
39
	/** @var int The message id when in the form msg123 */
40
	protected $_virtual_msg = 0;
41
42
	/** @var int Show signatures? */
43
	protected $_show_signatures = 0;
44
45
	/** @var int|string Start viewing the topics from ... (page, all, other) */
46
	protected $_start;
47
48
	/** @var bool if to include unapproved posts in the count  */
49
	protected $includeUnapproved;
50
51
	/** @var array data returned from getTopicInfo() */
52
	protected $topicinfo;
53
54
	/** @var int number of messages to show per topic page */
55
	protected $messages_per_page;
56
57
	/** @var int message number to start the listing from */
58
	protected $start_from;
59
60
	/**
61
	 * Default action handler for this controller, if its called directly
62
	 */
63
	public function action_index()
64
	{
65
		// what to do... display things!
66
		$this->action_display();
67
	}
68
69
	/**
70
	 * The central part of the board - topic display.
71
	 *
72
	 * What it does:
73 2
	 *
74
	 * - This function loads the posts in a topic, so they can be displayed.
75
	 * - It requires a topic, and can go to the previous or next topic from it.
76 2
	 * - It jumps to the correct post depending on a number/time/IS_MSG passed.
77 2
	 * - It depends on the messages_per_page, defaultMaxMessages and enableAllMessages settings.
78
	 * - It is accessed by ?topic=id_topic.START.
79
	 *
80
	 * @uses the main sub template of the Display template.
81
	 */
82
	public function action_display()
83
	{
84
		global $txt, $modSettings, $context, $settings, $options, $topic, $board;
85
		global $messages_request;
86
87
		$this->_events->trigger('pre_load', array('_REQUEST' => &$_REQUEST, 'topic' => $topic, 'board' => &$board));
88
89
		// What are you gonna display if these are empty?!
90
		if (empty($topic))
91
		{
92 2
			throw new Exception('no_board', false);
93
		}
94 2
95 2
		// And the topic functions
96 2
		require_once(SUBSDIR . '/Topic.subs.php');
97
		require_once(SUBSDIR . '/Messages.subs.php');
98 2
99
		// link prefetch is slower for the server, and it makes it impossible to know if they read it.
100
		stop_prefetching();
101 2
102
		// How much are we sticking on each page?
103
		$this->setMessagesPerPage();
104
		$this->includeUnapproved = $this->getIncludeUnapproved();
105
		$this->_start = $this->_req->getQuery('start');
106
107 2
		// Find the previous or next topic.  Make a fuss if there are no more.
108 2
		$this->getPreviousNextTopic();
109
110
		// Add 1 to the number of views of this topic (except for robots).
111 2
		$this->increaseTopicViews($topic);
112 2
113
		// Time to load the topic information
114
		$this->loadTopicInfo($topic, $board);
115 2
116
		// Is this a moved topic that we are redirecting to or coming from?
117
		$this->handleRedirection();
118 2
119 2
		// Trigger the topicinfo event for display
120 2
		$this->_events->trigger('topicinfo', array('topicinfo' => &$this->topicinfo, 'includeUnapproved' => $this->includeUnapproved));
121 2
122
		// If this topic has unapproved posts, we need to work out how many posts the user can see, for page indexing.
123
		$total_visible_posts = $this->getVisiblePosts($this->topicinfo['num_replies']);
124 2
125
		// The start isn't a number; it's information about what to do, where to go.
126 2
		$this->makeStartAdjustments($total_visible_posts);
127
128 2
		// Censor the title...
129
		$this->topicinfo['subject'] = censor($this->topicinfo['subject']);
130 2
131
		// Allow addons access to the topicinfo array
132
		call_integration_hook('integrate_display_topic', array($this->topicinfo));
133
134
		// If all is set, figure out what needs to be done
135 2
		$can_show_all = $this->getCanShowAll($total_visible_posts);
136 2
		$this->setupShowAll($can_show_all, $total_visible_posts);
137
138
		// Time to place all the particulars into context for the template
139
		$this->setMessageContext($topic, $total_visible_posts, $can_show_all);
140
141
		// Calculate the fastest way to get the messages!
142 2
		$ascending = true;
143
		$start = $this->_start;
144
		$limit = $this->messages_per_page;
145 2
		$firstIndex = 0;
146
147
		if ($start >= $total_visible_posts / 2 && $this->messages_per_page !== -1)
148
		{
149
			$ascending = false;
150
			$limit = $total_visible_posts <= $start + $limit ? $total_visible_posts - $start : $limit;
151
			$start = $total_visible_posts <= $start + $limit ? 0 : $total_visible_posts - $start - $limit;
152
			$firstIndex = $limit - 1;
153
		}
154 2
155
		// Taking care of member specific settings
156
		$limit_settings = array(
157
			'messages_per_page' => $this->messages_per_page,
158 2
			'start' => $start,
159
			'offset' => $limit,
160 2
		);
161 2
162
		// Get each post and poster in this topic.
163
		$topic_details = getTopicsPostsAndPoster($this->topicinfo['id_topic'], $limit_settings, $ascending);
164 2
		$messages = $topic_details['messages'];
165 2
166
		// Add the viewing member so their information is available for use in QR
167 2
		$posters = array_unique($topic_details['all_posters'] + [-1 => $this->user->id]);
168 2
		$all_posters = $topic_details['all_posters'];
0 ignored issues
show
Unused Code introduced by
The assignment to $all_posters is dead and can be removed.
Loading history...
169 2
		unset($topic_details);
170
171
		// Default this topic to not marked for notifications... of course...
172
		$context['is_marked_notify'] = false;
173 2
174
		$messages_request = false;
175
		$context['first_message'] = 0;
176 2
		$context['first_new_message'] = false;
177 2
178
		call_integration_hook('integrate_display_message_list', array(&$messages, &$posters));
179
180
		// If there _are_ messages here... (probably an error otherwise :!)
181
		if (!empty($messages))
182
		{
183 2
			// Mark the board as read or not ... calls updateReadNotificationsFor() sets $context['is_marked_notify']
184
			$this->markRead($messages, $board);
185
186
			$msg_parameters = array(
187
				'message_list' => $messages,
188
				'new_from' => $this->topicinfo['new_from'],
189 2
			);
190 2
			$msg_selects = array();
191 2
			$msg_tables = array();
192 2
			call_integration_hook('integrate_message_query', array(&$msg_selects, &$msg_tables, &$msg_parameters));
193 2
194
			MembersList::loadGuest();
195
196
			// What?  It's not like it *couldn't* be only guests in this topic...
197
			if (!empty($posters))
198
			{
199
				MembersList::load($posters);
200
			}
201
202
			// Load in the likes for this group of messages
203
			// If using quick reply, load the user into context for the poster area
204
			$this->prepareQuickReply();
205 2
206 2
			$messages_request = loadMessageRequest($msg_selects, $msg_tables, $msg_parameters);
207
208 2
			// Go to the last message if the given time is beyond the time of the last message.
209
			if ($this->start_from >= $this->topicinfo['num_replies'])
210
			{
211 2
				$this->start_from = $this->topicinfo['num_replies'];
212
			}
213
214
			// Since the anchor information is needed on the top of the page we load these variables beforehand.
215
			$context['first_message'] = $messages[$firstIndex] ?? $messages[0];
216
			$context['first_new_message'] = (int) $this->_start === (int) $this->start_from;
217 2
		}
218
219
		// Are we showing the signatures?
220
		$this->setSignatureShowStatus();
221
222
		// Set the callback.  (do you REALIZE how much memory all the messages would take?!?)
223 2
		// This will be called from the template.
224
		$bodyParser = new Normal(array(), false);
225
		$opt = new ValuesContainer([
226
			'icon_sources' => new MessageTopicIcons(!empty($modSettings['messageIconChecks_enable']), $settings['theme_dir']),
227
			'show_signatures' => $this->_show_signatures,
228
		]);
229 2
		$renderer = new DisplayRenderer($messages_request, $this->user, $bodyParser, $opt);
230
231
		$context['get_message'] = array($renderer, 'getContext');
232
233 2
		// Now set all the wonderful, wonderful permissions... like moderation ones...
234
		$this->setTopicCanPermissions();
235 2
236 2
		// Load up the Quick ModifyTopic and Quick Reply scripts
237
		loadJavascriptFile('topic.js');
238
239
		// Create the editor for the QR area
240
		$editorOptions = array(
241
			'id' => 'message',
242
			'value' => '',
243
			'labels' => array(
244 2
				'post_button' => $txt['post'],
245
			),
246
			// add height and width for the editor
247 2
			'height' => '250px',
248
			'width' => '100%',
249
			// We do XML preview here.
250 2
			'preview_type' => 1,
251
		);
252
253
		// Trigger the prepare_context event for modules that have tied in to it
254
		$this->_events->trigger('prepare_context', array('editorOptions' => &$editorOptions, 'use_quick_reply' => !empty($options['display_quick_reply'])));
255
256
		// Load up the "double post" sequencing magic.
257
		if (!empty($options['display_quick_reply']))
258 2
		{
259
			checkSubmitOnce('register');
260
			$context['name'] = $_SESSION['guest_name'] ?? '';
261
			$context['email'] = $_SESSION['guest_email'] ?? '';
262
			if (!empty($options['use_editor_quick_reply']) && $context['can_reply'])
263 2
			{
264
				// Needed for the editor and message icons.
265
				require_once(SUBSDIR . '/Editor.subs.php');
266
267
				create_control_richedit($editorOptions);
268
			}
269
		}
270
271
		theme()->addJavascriptVar(array('notification_topic_notice' => $context['is_marked_notify'] ? $txt['notification_disable_topic'] : $txt['notification_enable_topic']), true);
272
273
		if ($context['can_send_topic'])
274
		{
275
			theme()->addJavascriptVar(array(
276
				'sendtopic_cancel' => $txt['modify_cancel'],
277
				'sendtopic_back' => $txt['back'],
278 2
				'sendtopic_close' => $txt['find_close'],
279
				'sendtopic_error' => $txt['send_error_occurred'],
280 2
				'required_field' => $txt['require_field']), true);
281 2
		}
282
283
		// Build the common to all buttons like Reply Notify Mark ....
284
		$this->buildNormalButtons();
285 2
286
		// Build specialized buttons, like moderation
287 2
		$this->buildModerationButtons();
288
289
		// Load the template
290
		theme()->getTemplates()->load('Display');
291
		$this->_template_layers = theme()->getLayers();
292
		$this->_template_layers->addEnd('messages_informations');
293
		$context['sub_template'] = 'messages';
294
295
		// Let's get nosey, who is viewing this topic?
296 2
		if (!empty($settings['display_who_viewing']))
297
		{
298
			require_once(SUBSDIR . '/Who.subs.php');
299
			formatViewers($this->topicinfo['id_topic'], 'topic');
300
		}
301 2
302
		// Did we report a post to a moderator just now?
303
		if (isset($this->_req->query->reportsent))
304 2
		{
305 2
			$this->_template_layers->add('report_sent');
306
		}
307
308
		// Quick reply & modify enabled?
309
		if ($context['can_reply'] && !empty($options['display_quick_reply']))
310 2
		{
311 2
			loadJavascriptFile('mentioning.js');
312
			$this->_template_layers->addBefore('quickreply', 'messages_informations');
313
		}
314
315
		// All of our buttons and indexes
316
		$this->_template_layers->add('pages_and_buttons');
317 2
	}
318 2
319
	/**
320
	 * Sets the message per page
321 2
	 */
322 2
	public function setMessagesPerPage()
323
	{
324
		global $modSettings, $options;
325 2
326
		$this->messages_per_page = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? (int) $options['messages_per_page'] : (int) $modSettings['defaultMaxMessages'];
327
	}
328 2
329
	/**
330
	 * Returns IF we are counting unapproved posts
331 2
	 *
332 2
	 * @return bool
333
	 */
334
	public function getIncludeUnapproved()
335
	{
336
		global $modSettings;
337
338 2
		return !$modSettings['postmod_active'] || allowedTo('approve_posts');
339
	}
340 2
341 2
	/**
342
	 * Return if we allow showing ALL messages for a topic vs pagination
343
	 *
344
	 * @param int $total_visible_posts
345 2
	 * @return bool
346 2
	 */
347
	public function getCanShowAll($total_visible_posts)
348
	{
349
		global $modSettings;
350
351 2
		return !empty($modSettings['enableAllMessages'])
352
			&& $total_visible_posts > $this->messages_per_page
353
			&& $total_visible_posts < $modSettings['enableAllMessages'];
354
	}
355
356
	/**
357 2
	 * If show all is requested, and allowed, setup to do just that
358 2
	 *
359
	 * @param bool $can_show_all
360
	 * @param int $total_visible_posts
361
	 * @return void
362 2
	 */
363 2
	public function setupShowAll($can_show_all, $total_visible_posts)
364 2
	{
365
		global $scripturl, $topic, $context;
366
367
		$all_requested = $this->_req->getQuery('all', 'trim', null);
368
		if (isset($all_requested))
369 2
		{
370 2
			// If all is set, but not allowed... just unset it.
371
			if (!$can_show_all)
372
			{
373
				unset($all_requested);
374 2
			}
375
			else
376
			{
377
				// Otherwise, it must be allowed... so pretend start was -1.
378
				$this->_start = -1;
379
			}
380
		}
381
382
		// Construct the page index, allowing for the .START method...
383
		$context['page_index'] = constructPageIndex($scripturl . '?topic=' . $topic . '.%1$d', $this->_start, $total_visible_posts, $this->messages_per_page, true, array('all' => $can_show_all, 'all_selected' => isset($all_requested)));
384 2
		$context['start'] = $this->_start;
385 2
386 2
		// Figure out all the link to the next/prev
387
		$context['links'] += array(
388
			'prev' => $this->_start >= $this->messages_per_page ? $scripturl . '?topic=' . $topic . '.' . ($this->_start - $this->messages_per_page) : '',
389
			'next' => $this->_start + $this->messages_per_page < $total_visible_posts ? $scripturl . '?topic=' . $topic . '.' . ($this->_start + $this->messages_per_page) : '',
390 2
		);
391 2
392
		// If they are viewing all the posts, show all the posts, otherwise limit the number.
393
		if ($can_show_all && isset($all_requested))
394 2
		{
395 2
			// No limit! (actually, there is a limit, but...)
396 2
			$this->messages_per_page = -1;
397 2
398 2
			// Set start back to 0...
399
			$this->_start = 0;
400 2
		}
401
	}
402
403 2
	/**
404 2
	 * Returns the previous or next topic based on the get/query value
405 2
	 * @return void
406 2
	 */
407
	public function getPreviousNextTopic()
408
	{
409 2
		global $board_info, $topic, $board, $context;
410
411
		$prev_next = $this->_req->getQuery('prev_next', 'trim');
412 2
413
		// Find the previous or next topic.  Make a fuss if there are no more.
414
		if ($prev_next === 'prev' || $prev_next === 'next')
415 2
		{
416 2
			// No use in calculating the next topic if there's only one.
417 2
			if ($board_info['num_topics'] > 1)
418 2
			{
419 2
				$topic = $prev_next === 'prev'
420
					? previousTopic($topic, $board, $this->user->id, $this->getIncludeUnapproved())
421
					: nextTopic($topic, $board, $this->user->id, $this->getIncludeUnapproved());
422
423
				$context['current_topic'] = $topic;
424
			}
425
426
			// Go to the newest message on this topic.
427
			$this->_start = 'new';
428
		}
429 2
	}
430 2
431 2
	/**
432
	 * Add one for the stats
433
	 * @param $topic
434
	 */
435 2
	public function increaseTopicViews($topic)
436 2
	{
437 2
		if ($this->user->possibly_robot === false
438 2
			&& (empty($_SESSION['last_read_topic']) || $_SESSION['last_read_topic'] !== $topic))
439 2
		{
440
			increaseViewCounter($topic);
441 2
			$_SESSION['last_read_topic'] = $topic;
442
		}
443
	}
444 2
445
	/**
446 2
	 * Fetch all the topic information.  Provides addons a hook to add additional tables/selects
447
	 *
448 2
	 * @param int $topic
449 2
	 * @param int $board
450
	 * @throws \ElkArte\Exceptions\Exception on invalid topic value
451 2
	 */
452
	public function loadTopicInfo($topic, $board)
453 2
	{
454
		$topic_selects = [];
455 2
		$topic_tables = [];
456 2
		$topic_parameters = [
457
			'topic' => $topic,
458 2
			'member' => $this->user->id,
459
			'board' => (int) $board,
460 2
		];
461
462
		// Allow addons to add additional details to the topic query
463
		call_integration_hook('integrate_topic_query', array(&$topic_selects, &$topic_tables, &$topic_parameters));
464 2
465
		// Load the topic details
466
		$this->topicinfo = getTopicInfo($topic_parameters, 'all', $topic_selects, $topic_tables);
0 ignored issues
show
Documentation Bug introduced by
It seems like getTopicInfo($topic_para...selects, $topic_tables) can also be of type boolean. However, the property $topicinfo is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
467 2
468
		// Nothing??
469 2
		if (empty($this->topicinfo))
470 2
		{
471
			throw new Exception('not_a_topic', false);
472
		}
473
	}
474 2
475
	/**
476
	 * Sometimes topics have been moved, this will direct the user to the right spot
477 2
	 */
478
	public function handleRedirection()
479 2
	{
480
		global $context;
481
482 2
		// Need to send the user to the new location?
483
		if (!empty($this->topicinfo['id_redirect_topic']) && !isset($this->_req->query->noredir))
484
		{
485
			markTopicsRead(array($this->user->id, $this->topicinfo['id_topic'], $this->topicinfo['id_last_msg'], 0), $this->topicinfo['new_from'] !== 0);
486
			redirectexit('topic=' . $this->topicinfo['id_redirect_topic'] . '.0;redirfrom=' . $this->topicinfo['id_topic']);
487
		}
488
489 2
		// Or are we here because we were redirected?
490
		if (isset($this->_req->query->redirfrom))
491
		{
492
			$redirfrom = $this->_req->getQuery('redirfrom', 'intval');
493 2
			$redir_topics = topicsList(array($redirfrom));
494 2
			if (!empty($redir_topics[$redirfrom]))
495
			{
496 2
				$context['topic_redirected_from'] = $redir_topics[$redirfrom];
497 2
				$context['topic_redirected_from']['redir_href'] = getUrl('topic', ['topic' => $context['topic_redirected_from']['id_topic'], 'start' => '0', 'subject' => $context['topic_redirected_from']['subject'], 'noredir']);
498 2
			}
499
		}
500
	}
501 2
502
	/**
503
	 * Number of posts that this user can see.  Will included unapproved for those with access
504
	 *
505
	 * @param int $num_replies
506
	 * @return int
507 2
	 */
508
	public function getVisiblePosts($num_replies)
509 2
	{
510 2
		if (!$this->includeUnapproved && $this->topicinfo['unapproved_posts'] && $this->user->is_guest === false)
511
		{
512
			$myUnapprovedPosts = unapprovedPosts($this->topicinfo['id_topic'], $this->user->id);
513 2
514 2
			return $num_replies + $myUnapprovedPosts + ($this->topicinfo['approved'] ? 1 : 0);
515 2
		}
516
517 2
		if ($this->user->is_guest)
518
		{
519
			return $num_replies + ($this->topicinfo['approved'] ? 1 : 0);
520 2
		}
521
522
		return $num_replies + $this->topicinfo['unapproved_posts'] + ($this->topicinfo['approved'] ? 1 : 0);
523
	}
524 2
525 2
	/**
526 2
	 * The start value from get can contain all manner of information on what to do.
527
	 * This converts new, from, msg into something useful, most times.
528
	 *
529
	 * @param int $total_visible_posts
530
	 */
531
	public function makeStartAdjustments($total_visible_posts)
532
	{
533
		global $modSettings;
534
535
		$start = $this->_start;
536
		if (!is_numeric($start))
537 2
		{
538
			// Redirect to the page and post with new messages
539
			if ($start === 'new')
540 2
			{
541
				// Guests automatically go to the last post.
542
				if ($this->user->is_guest)
543 2
				{
544
					$start = $total_visible_posts - 1;
545 2
				}
546
				else
547
				{
548
					// Fall through to the next if statement.
549 2
					$start = 'msg' . $this->topicinfo['new_from'];
550 2
				}
551
			}
552
553
			// Start from a certain time index, not a message.
554
			if (strpos($start, 'from') === 0)
555
			{
556
				$timestamp = (int) substr($start, 4);
557
				if ($timestamp === 0)
558
				{
559 2
					$start = 0;
560 2
				}
561 2
				else
562 2
				{
563
					// Find the number of messages posted before said time...
564 2
					$start = countNewPosts($this->topicinfo['id_topic'], $this->topicinfo, $timestamp);
565 2
				}
566
			}
567 2
			// Link to a message...
568 2
			elseif (strpos($start, 'msg') === 0)
569
			{
570
				$this->_virtual_msg = (int) substr($start, 3);
571
				if (!$this->topicinfo['unapproved_posts'] && $this->_virtual_msg >= $this->topicinfo['id_last_msg'])
572
				{
573
					$start = $total_visible_posts - 1;
574 2
				}
575
				elseif (!$this->topicinfo['unapproved_posts'] && $this->_virtual_msg <= $this->topicinfo['id_first_msg'])
576
				{
577
					$start = 0;
578
				}
579 2
				else
580 2
				{
581 2
					$only_approved = $modSettings['postmod_active'] && $this->topicinfo['unapproved_posts'] && !allowedTo('approve_posts');
582 2
					$start = countMessagesBefore($this->topicinfo['id_topic'], $this->_virtual_msg, false, $only_approved, $this->user->is_guest === false);
583
				}
584 2
			}
585
		}
586 2
587
		$this->_start = $start;
588
	}
589
590 2
	/**
591
	 * Sets all we know about a message into $context for template consumption.
592
	 * Note: After this processes, some amount of additional context is still added, read
593
	 * the code.
594
	 *
595
	 * @param int $topic the topic id
596
	 * @param int $total_visible_posts How many are in this topic
597
	 * @param bool $can_show_all If they can show all or need to follow pagination
598
	 */
599
	public function setMessageContext($topic, $total_visible_posts, $can_show_all)
600
	{
601
		global $context, $modSettings, $txt, $board_info;
602
603
		// Going to allow this to be indexed by Mr. Robot?
604
		$context['robot_no_index'] = $this->setRobotNoIndex();
605 2
606
		// Some basics for the template
607 2
		$context['num_replies'] = $this->topicinfo['num_replies'];
608
		$context['topic_first_message'] = $this->topicinfo['id_first_msg'];
609
		$context['topic_last_message'] = $this->topicinfo['id_last_msg'];
610
		$context['topic_unwatched'] = $this->topicinfo['unwatched'] ?? 0;
611
		$context['start_from'] = $this->start_from;
612 2
613
		// Did this user start the topic or not?
614
		$context['user']['started'] = $this->didThisUserStart();
615
		$context['topic_starter_id'] = $this->topicinfo['id_member_started'];
616
617
		// Add up unapproved replies to get real number of replies...
618 2
		$context['real_num_replies'] = $this->topicinfo['num_replies'];
619
		if ($modSettings['postmod_active'] && allowedTo('approve_posts'))
620 2
		{
621
			$context['real_num_replies'] += $this->topicinfo['unapproved_posts'] - ($this->topicinfo['approved'] ? 0 : 1);
622
		}
623
624 2
		// When was the last time this topic was replied to?  Should we warn them about it?
625 2
		$context['oldTopicError'] = $this->warnOldTopic();
626 2
627 2
		// Are we showing signatures - or disabled fields?
628
		$context['signature_enabled'] = strpos($modSettings['signature_settings'], '1') === 0;
629
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : [];
630 2
631
		// Page title
632
		$context['page_title'] = $this->topicinfo['subject'];
633 2
634
		// Create a previous next string if the selected theme has it as a selected option.
635
		if ($modSettings['enablePreviousNext'])
636
		{
637
			$context['links'] += array(
638 2
				'go_prev' => getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => '0', 'subject' => $this->topicinfo['subject'], 'prev_next' => 'prev']) . '#new',
639 2
				'go_next' => getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => '0', 'subject' => $this->topicinfo['subject'], 'prev_next' => 'next']) . '#new'
640 2
			);
641 2
		}
642 2
643 2
		// Build the jump to box
644
		$context['jump_to'] = array(
645
			'label' => addslashes(un_htmlspecialchars($txt['jump_to'])),
646 2
			'board_name' => htmlspecialchars(strtr(strip_tags($board_info['name']), array('&amp;' => '&')), ENT_COMPAT),
647
			'child_level' => $board_info['child_level'],
648
		);
649 2
650 2
		// Build a list of this board's moderators.
651
		$context['moderators'] = &$board_info['moderators'];
652
		$context['link_moderators'] = [];
653 2
654
		// Information about the current topic...
655
		$context['is_locked'] = $this->topicinfo['locked'];
656 2
		$context['is_sticky'] = $this->topicinfo['is_sticky'];
657
		$context['is_very_hot'] = $this->topicinfo['num_replies'] >= $modSettings['hotTopicVeryPosts'];
658
		$context['is_hot'] = $this->topicinfo['num_replies'] >= $modSettings['hotTopicPosts'];
659
		$context['is_approved'] = $this->topicinfo['approved'];
660
661
		// Set the class of the current topic,  Hot, not so hot, locked, sticky
662
		determineTopicClass($context);
663
664
		// Set the topic's information for the template.
665
		$context['subject'] = $this->topicinfo['subject'];
666 2
		$context['num_views'] = $this->topicinfo['num_views'];
667 2
		$context['num_views_text'] = (int) $this->topicinfo['num_views'] === 1 ? $txt['read_one_time'] : sprintf($txt['read_many_times'], $this->topicinfo['num_views']);
668
		$context['mark_unread_time'] = !empty($this->_virtual_msg) ? $this->_virtual_msg : $this->topicinfo['new_from'];
669 2
670
		// Set a canonical URL for this page.
671
		$context['canonical_url'] = getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => $context['start'], 'subject' => $this->topicinfo['subject']]);
672 2
673 2
		// For quick reply we need a response prefix in the default forum language.
674
		$context['response_prefix'] = response_prefix();
675 2
676
		$context['messages_per_page'] = $this->messages_per_page;
677
678
		// Build the link tree.
679 2
		$context['linktree'][] = array(
680
			'url' => getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => '0', 'subject' => $this->topicinfo['subject']]),
681
			'name' => $this->topicinfo['subject'],
682 2
		);
683
	}
684 2
685 2
	/**
686 2
	 * Sets if this is a page that we do or do not want bots to index
687 2
	 *
688
	 * @return bool
689
	 */
690
	public function setRobotNoIndex()
691
	{
692
		// Let's do some work on what to search index.
693
		if (count((array) $this->_req->query) > 2)
694
		{
695
			foreach (['topic', 'board', 'start', session_name()] as $key)
696 2
			{
697
				if (!isset($this->_req->query->$key))
698 2
				{
699
					return true;
700 2
				}
701 2
			}
702 2
		}
703 2
704 2
		return !empty($this->_start)
705 2
			&& (!is_numeric($this->_start) || $this->_start % $this->messages_per_page !== 0);
706
	}
707
708
	/**
709 2
	 * Return if the current user started this topic, as that may provide them additional permissions.
710 1
	 *
711 2
	 * @return bool
712 2
	 */
713 2
	public function didThisUserStart()
714
	{
715 2
		return ((int) $this->user->id === (int) $this->topicinfo['id_member_started']) && !$this->user->is_guest;
716
	}
717
718
	/**
719 2
	 * They Hey bub, what's with the necro-bump message
720 2
	 *
721 2
	 * @return bool
722
	 */
723 2
	public function warnOldTopic()
724 2
	{
725
		global $modSettings;
726
727 2
		if (!empty($modSettings['oldTopicDays']))
728 2
		{
729 2
			$mgsOptions = basicMessageInfo($this->topicinfo['id_last_msg'], true);
730
731 2
			return $mgsOptions['poster_time'] + $modSettings['oldTopicDays'] * 86400 < time()
732
				&& empty($this->topicinfo['is_sticky']);
733
		}
734 2
735 2
		return false;
736 2
	}
737
738 2
	/**
739 2
	 * Keeps track of where the user is in reading this topic.
740
	 *
741
	 * @param array $messages
742 2
	 * @param int $board
743 2
	 */
744 2
	private function markRead($messages, $board)
745
	{
746 2
		global $modSettings;
747 2
748
		// Guests can't mark topics read or for notifications, just can't sorry.
749
		if ($this->user->is_guest === false && !empty($messages))
750 2
		{
751 2
			$boardseen = isset($this->_req->query->boardseen);
752 2
753
			$mark_at_msg = max($messages);
754 2
			if ($mark_at_msg >= $this->topicinfo['id_last_msg'])
755 2
			{
756 2
				$mark_at_msg = $modSettings['maxMsgID'];
757
			}
758
759
			if ($mark_at_msg >= $this->topicinfo['new_from'])
760
			{
761 2
				markTopicsRead(array($this->user->id, $this->topicinfo['id_topic'], $mark_at_msg, $this->topicinfo['unwatched']), $this->topicinfo['new_from'] !== 0);
762 1
				$numNewTopics = getUnreadCountSince($board, empty($_SESSION['id_msg_last_visit']) ? 0 : $_SESSION['id_msg_last_visit']);
763 2
764 2
				if (empty($numNewTopics))
765 2
				{
766
					$boardseen = true;
767 2
				}
768
			}
769
770 2
			updateReadNotificationsFor($this->topicinfo['id_topic'], $board);
771 2
772 2
			// Mark board as seen if we came using last post link from BoardIndex. (or other places...)
773
			if ($boardseen)
774 2
			{
775 2
				require_once(SUBSDIR . '/Boards.subs.php');
776
				markBoardsRead($board, false, false);
777
			}
778 2
		}
779 2
	}
780 2
781
	/**
782 2
	 * If the QR is on, we need to load the user information into $context, so we
783
	 * can show the new improved 2.0 QR area
784
	 */
785 2
	public function prepareQuickReply()
786 2
	{
787 2
		global $options, $context;
788
789 2
		if (empty($options['hide_poster_area']) && $options['display_quick_reply'])
790
		{
791
			// First lets load the profile array
792 2
			$thisUser = MembersList::get(User::$info->id);
793 2
			$thisUser->loadContext();
794 2
			$context['thisMember'] = [
795
				'id' => 'new',
796 2
				'is_message_author' => true,
797
				'member' => $thisUser->toArray()['data']
798
			];
799
		}
800
	}
801 2
802
	/**
803
	 * Sets if we are showing signatures or not
804
	 */
805
	public function setSignatureShowStatus()
806
	{
807
		global $modSettings;
808
809
		list ($sig_limits) = explode(':', $modSettings['signature_settings']);
810
		$signature_settings = explode(',', $sig_limits);
811
		if ($this->user->is_guest)
812 2
		{
813
			$this->_show_signatures = !empty($signature_settings[8]) ? (int) $signature_settings[8] : 0;
814 2
		}
815
		else
816
		{
817 2
			$this->_show_signatures = !empty($signature_settings[9]) ? (int) $signature_settings[9] : 0;
818
		}
819
	}
820 2
821 2
	/**
822 2
	 * Loads into context the various message/topic permissions so the template
823
	 * knows what buttons etc. to show
824
	 */
825
	public function setTopicCanPermissions()
826
	{
827
		global $modSettings, $context, $settings, $board;
828
829
		// First the common ones
830
		$common_permissions = array(
831
			'can_approve' => 'approve_posts',
832
			'can_ban' => 'manage_bans',
833
			'can_sticky' => 'make_sticky',
834
			'can_merge' => 'merge_any',
835
			'can_split' => 'split_any',
836
			'can_mark_notify' => 'mark_any_notify',
837
			'can_send_topic' => 'send_topic',
838
			'can_send_pm' => 'pm_send',
839
			'can_send_email' => 'send_email_to_members',
840
			'can_report_moderator' => 'report_any',
841
			'can_moderate_forum' => 'moderate_forum',
842
			'can_issue_warning' => 'issue_warning',
843
			'can_restore_topic' => 'move_any',
844
			'can_restore_msg' => 'move_any',
845
		);
846
		foreach ($common_permissions as $contextual => $perm)
847
		{
848
			$context[$contextual] = allowedTo($perm);
849
		}
850
851
		// Permissions with _any/_own versions.  $context[YYY] => ZZZ_any/_own.
852
		$anyown_permissions = array(
853
			'can_move' => 'move',
854
			'can_lock' => 'lock',
855
			'can_delete' => 'remove',
856
			'can_reply' => 'post_reply',
857
			'can_reply_unapproved' => 'post_unapproved_replies',
858
		);
859
		foreach ($anyown_permissions as $contextual => $perm)
860
		{
861
			$context[$contextual] = allowedTo($perm . '_any') || ($this->didThisUserStart() && allowedTo($perm . '_own'));
862
		}
863
864
		// Cleanup all the permissions with extra stuff...
865
		$context['can_mark_notify'] &= !$context['user']['is_guest'];
866
		$context['can_reply'] &= empty($this->topicinfo['locked']) || allowedTo('moderate_board');
867
		$context['can_reply_unapproved'] &= $modSettings['postmod_active'] && (empty($this->topicinfo['locked']) || allowedTo('moderate_board'));
868
		$context['can_issue_warning'] &= featureEnabled('w') && !empty($modSettings['warning_enable']);
869
870
		// Handle approval flags...
871
		$context['can_reply_approved'] = $context['can_reply'];
872
873
		// Guests do not have post_unapproved_replies_own permission, so it's always post_unapproved_replies_any
874
		if ($this->user->is_guest && allowedTo('post_unapproved_replies_any'))
875
		{
876
			$context['can_reply_approved'] = false;
877
		}
878
879
		$context['can_reply'] |= $context['can_reply_unapproved'];
880
		$context['can_quote'] = $context['can_reply'] && (empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC'])));
881
		$context['can_mark_unread'] = $this->user->is_guest === false && $settings['show_mark_read'];
882
		$context['can_unwatch'] = $this->user->is_guest === false && $modSettings['enable_unwatch'];
883
		$context['can_send_topic'] = (!$modSettings['postmod_active'] || $this->topicinfo['approved']) && allowedTo('send_topic');
884
		$context['can_print'] = empty($modSettings['disable_print_topic']);
885
886
		// Start this off for quick moderation - it will be or'd for each post.
887
		$context['can_remove_post'] = allowedTo('delete_any') || (allowedTo('delete_replies') && $this->didThisUserStart());
888
889
		// Can restore topic?  That's if the topic is in the recycle board and has a previous restore state.
890
		$context['can_restore_topic'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($this->topicinfo['id_previous_board']);
891
		$context['can_restore_msg'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($this->topicinfo['id_previous_topic']);
892
	}
893
894
	/**
895
	 * Loads into $context the normal button array for template use.
896
	 * Calls integrate_display_buttons hook
897
	 */
898
	public function buildNormalButtons()
899
	{
900
		global $context, $txt;
901
902
		// Build the normal button array.
903
		$context['normal_buttons'] = array(
904
			'reply' => array(
905
				'test' => 'can_reply',
906
				'text' => 'reply',
907
				'lang' => true,
908
				'url' => getUrl('action', ['action' => 'post', 'topic' => $context['current_topic'] . '.' . $context['start'], 'last_msg' => $this->topicinfo['id_last_msg']]),
909
				'active' => true
910
			),
911
			'notify' => array(
912
				'test' => 'can_mark_notify',
913
				'text' => $context['is_marked_notify'] ? 'unnotify' : 'notify',
914
				'lang' => true,
915
				'custom' => 'onclick="return notifyButton(this);"',
916
				'url' => getUrl('action', ['action' => 'notify', 'sa' => $context['is_marked_notify'] ? 'off' : 'on', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
917
			),
918
			'mark_unread' => array(
919
				'test' => 'can_mark_unread',
920
				'text' => 'mark_unread',
921
				'lang' => true,
922
				'url' => getUrl('action', ['action' => 'markasread', 'sa' => 'topic', 't' => $context['mark_unread_time'], 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
923
			),
924
			'unwatch' => array(
925
				'test' => 'can_unwatch',
926
				'text' => ($context['topic_unwatched'] ? '' : 'un') . 'watch',
927
				'lang' => true,
928
				'custom' => 'onclick="return unwatchButton(this);"',
929
				'url' => getUrl('action', ['action' => 'unwatchtopic', 'sa' => $context['topic_unwatched'] ? 'off' : 'on', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
930
			),
931
			'send' => array(
932
				'test' => 'can_send_topic',
933
				'text' => 'send_topic',
934
				'lang' => true,
935
				'url' => getUrl('action', ['action' => 'emailuser', 'sa' => 'sendtopic', 'topic' => $context['current_topic'] . '.0']),
936
				'custom' => 'onclick="return sendtopicOverlayDiv(this.href, \'' . $txt['send_topic'] . '\');"'
937
			),
938
			'print' => array(
939
				'test' => 'can_print',
940
				'text' => 'print',
941
				'lang' => true,
942
				'custom' => 'rel="nofollow"',
943
				'class' => 'new_win',
944
				'url' => getUrl('action', ['action' => 'topic', 'sa' => 'printpage', 'topic' => $context['current_topic'] . '.0'])
945
			),
946
		);
947
948
		// Allow adding new buttons easily.
949
		call_integration_hook('integrate_display_buttons');
950
	}
951
952
	/**
953
	 * Loads into $context the moderation button array for template use.
954
	 * Call integrate_mod_buttons hook
955
	 */
956
	public function buildModerationButtons()
957
	{
958
		global $context, $txt;
959
960
		// Build the mod button array
961
		$context['mod_buttons'] = array(
962
			'move' => array(
963
				'test' => 'can_move',
964
				'text' => 'move_topic',
965
				'lang' => true,
966
				'url' => getUrl('action', ['action' => 'movetopic', 'current_board' => $context['current_board'], 'topic' => $context['current_topic'] . '.0'])
967
			),
968
			'delete' => array(
969
				'test' => 'can_delete',
970
				'text' => 'remove_topic',
971
				'lang' => true,
972
				'custom' => 'onclick="return confirm(\'' . $txt['are_sure_remove_topic'] . '\');"',
973
				'url' => getUrl('action', ['action' => 'removetopic2', 'topic' => $context['current_topic'] . '.0', '{session_data}'])
974
			),
975
			'lock' => array(
976
				'test' => 'can_lock',
977
				'text' => empty($this->topicinfo['locked']) ? 'set_lock' : 'set_unlock',
978
				'lang' => true,
979
				'url' => getUrl('action', ['action' => 'topic', 'sa' => 'lock', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
980
			),
981
			'sticky' => array(
982
				'test' => 'can_sticky',
983
				'text' => empty($this->topicinfo['is_sticky']) ? 'set_sticky' : 'set_nonsticky',
984
				'lang' => true,
985
				'url' => getUrl('action', ['action' => 'topic', 'sa' => 'sticky', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
986
			),
987
			'merge' => array(
988
				'test' => 'can_merge',
989
				'text' => 'merge',
990
				'lang' => true,
991
				'url' => getUrl('action', ['action' => 'mergetopics', 'board' => $context['current_board'] . '.0', 'from' => $context['current_topic']])
992
			),
993
		);
994
995
		// Restore topic. eh?  No monkey business.
996
		if ($context['can_restore_topic'])
997
		{
998
			$context['mod_buttons'][] = array(
999
				'text' => 'restore_topic',
1000
				'lang' => true,
1001
				'url' => getUrl('action', ['action' => 'restoretopic', 'topics' => $context['current_topic'], '{session_data}'])
1002
			);
1003
		}
1004
1005
		// Allow adding new buttons easily.
1006
		call_integration_hook('integrate_mod_buttons');
1007
	}
1008
1009
	/**
1010
	 * If we are in a topic and don't have permission to approve it then duck out now.
1011
	 * This is an abuse of the method, but it's easier that way.
1012
	 *
1013
	 * @param string $action the function name of the current action
1014
	 *
1015
	 * @return bool
1016
	 * @throws \ElkArte\Exceptions\Exception not_a_topic
1017
	 */
1018
	public function trackStats($action = '')
1019
	{
1020
		global $topic, $board_info;
1021
1022
		if (!empty($topic)
1023
			&& empty($board_info['cur_topic_approved'])
1024
			&& ($this->user->id != $board_info['cur_topic_starter'] || $this->user->is_guest)
1025
			&& !allowedTo('approve_posts'))
1026
		{
1027
			throw new Exception('not_a_topic', false);
1028
		}
1029
1030
		return parent::trackStats($action);
1031
	}
1032
1033
	/**
1034
	 * In-topic quick moderation.
1035
	 *
1036
	 * Accessed by ?action=quickmod2
1037
	 */
1038
	public function action_quickmod2()
1039
	{
1040
		global $topic, $board, $context, $modSettings;
1041
1042
		// Check the session = get or post.
1043
		checkSession('request');
1044
1045
		require_once(SUBSDIR . '/Messages.subs.php');
1046
1047
		if (empty($this->_req->post->msgs))
1048
		{
1049
			redirectexit('topic=' . $topic . '.' . $this->_req->getQuery('start', 'intval'));
1050
		}
1051
1052
		$messages = array_map('intval', $this->_req->post->msgs);
1053
1054
		// We are restoring messages. We handle this in another place.
1055
		if (isset($this->_req->query->restore_selected))
1056
		{
1057
			redirectexit('action=restoretopic;msgs=' . implode(',', $messages) . ';' . $context['session_var'] . '=' . $context['session_id']);
1058
		}
1059
1060
		if (isset($this->_req->query->split_selection))
1061
		{
1062
			$mgsOptions = basicMessageInfo(min($messages), true);
1063
1064
			$_SESSION['split_selection'][$topic] = $messages;
1065
			redirectexit('action=splittopics;sa=selectTopics;topic=' . $topic . '.0;subname_enc=' . urlencode($mgsOptions['subject']) . ';' . $context['session_var'] . '=' . $context['session_id']);
1066
		}
1067
1068
		require_once(SUBSDIR . '/Topic.subs.php');
1069
		$topic_info = getTopicInfo($topic);
1070
1071
		// Allowed to delete any message?
1072
		$allowed_all = $this->canDeleteAll($topic_info);
1073
1074
		// Make sure they're allowed to delete their own messages, if not any.
1075
		if (!$allowed_all)
1076
		{
1077
			isAllowedTo('delete_own');
1078
		}
1079
1080
		// Allowed to remove which messages?
1081
		$messages = determineRemovableMessages($topic, $messages, $allowed_all);
1082
1083
		// Get the first message in the topic - because you can't delete that!
1084
		$first_message = (int) $topic_info['id_first_msg'];
1085
		$last_message = (int) $topic_info['id_last_msg'];
1086
		$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
1087
1088
		// Delete all the messages we know they can delete. ($messages)
1089
		foreach ($messages as $message => $info)
1090
		{
1091
			$message = (int) $message;
1092
1093
			// Just skip the first message - if it's not the last.
1094
			if ($message === $first_message && $message !== $last_message)
1095
			{
1096
				continue;
1097
			}
1098
1099
			// If the first message is going then don't bother going back to the topic as we're effectively deleting it.
1100
			if ($message === $first_message)
1101
			{
1102
				$topicGone = true;
1103
			}
1104
1105
			$remover->removeMessage($message);
1106
		}
1107
1108
		redirectexit(!empty($topicGone) ? 'board=' . $board : 'topic=' . $topic . '.' . (int) $this->_req->query->start);
1109
	}
1110
1111
	/**
1112
	 * Determine if this user can delete all replies in this message
1113
	 *
1114
	 * @param array $topic_info
1115
	 * @return bool
1116
	 */
1117
	public function canDeleteAll($topic_info)
1118
	{
1119
		if (allowedTo('delete_any'))
1120
		{
1121
			return true;
1122
		}
1123
1124
		// Allowed to delete replies to their messages?
1125
		if (allowedTo('delete_replies'))
1126
		{
1127
			return (int) $topic_info['id_member_started'] === (int) $this->user->id;
1128
		}
1129
1130
		return false;
1131
	}
1132
}
1133