Display::canDeleteAll()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This controls topic display, with all related functions. It 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 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\Controller;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Exceptions\Exception;
21
use ElkArte\Helper\Util;
22
use ElkArte\Helper\ValuesContainer;
23
use ElkArte\MembersList;
24
use ElkArte\MessagesCallback\BodyParser\Normal;
25
use ElkArte\MessagesCallback\DisplayRenderer;
26
use ElkArte\MessagesDelete;
27
use ElkArte\MessageTopicIcons;
28
use ElkArte\User;
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 it's 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(): void
83
	{
84
		global $txt, $modSettings, $context, $settings, $options, $topic, $board;
85
		global $messages_request;
86
87
		$this->_events->trigger('pre_load', ['_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', ['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 subject...
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', [$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();
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
			$start = ($start >= $total_visible_posts) ? $total_visible_posts - $limit : $start;
150
			$ascending = false;
151
			$limit = $total_visible_posts <= $start + $limit ? $total_visible_posts - $start : $limit;
152
			$start = $total_visible_posts <= $start + $limit ? 0 : $total_visible_posts - $start - $limit;
153
			$firstIndex = $limit - 1;
154 2
		}
155
156
		// Taking care of member-specific settings
157
		$limit_settings = [
158 2
			'messages_per_page' => $this->messages_per_page,
159
			'start' => $start,
160 2
			'offset' => $limit,
161 2
		];
162
163
		// Get each post and poster on this topic page.
164 2
		$topic_details = getTopicsPostsAndPoster($this->topicinfo['id_topic'], $limit_settings, $ascending);
165 2
		$messages = $topic_details['messages'];
166
167 2
		// Add the viewing member so their information is available for use in enhanced QR
168 2
		$posters = array_unique($topic_details['all_posters'] + [-1 => $this->user->id]);
169 2
		$all_posters = $topic_details['all_posters'];
170
		unset($topic_details);
171
172
		// Default this topic to not mark for notifications... of course...
173 2
		$context['is_marked_notify'] = false;
174
175
		$messages_request = false;
176 2
		$context['first_message'] = 0;
177 2
		$context['first_new_message'] = false;
178
179
		call_integration_hook('integrate_display_message_list', [&$messages, &$all_posters]);
180
181
		// If there _are_ messages here... (probably an error otherwise!)
182
		if (!empty($messages))
183 2
		{
184
			// Mark the board as read or not ... calls updateReadNotificationsFor() sets $context['is_marked_notify']
185
			$this->markRead($messages, $board);
186
187
			// Mark notifications for this member and first seen messages
188
			$this->markNotificationsRead($messages);
189 2
190 2
			$msg_parameters = [
191 2
				'message_list' => $messages,
192 2
				'new_from' => $this->topicinfo['new_from'],
193 2
			];
194
			$msg_selects = [];
195
			$msg_tables = [];
196
			call_integration_hook('integrate_message_query', [&$msg_selects, &$msg_tables, &$msg_parameters]);
197
198
			MembersList::loadGuest();
199
200
			// What?  It's not like it *couldn't* be only guests in this topic...
201
			MembersList::load($posters);
202
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
				$context['start_from'] = $this->start_from;
213
			}
214
215
			// Since the anchor information is needed on the top of the page, we load these variables beforehand.
216
			$context['first_message'] = $messages[$firstIndex] ?? $messages[0];
217 2
			$context['first_new_message'] = (int) $this->_start === (int) $this->start_from;
218
		}
219
220
		// Are we showing the signatures?
221
		$this->setSignatureShowStatus();
222
223 2
		// Now set all the wonderful, wonderful permissions... like moderation ones...
224
		$this->setTopicCanPermissions();
225
226
		// Set the callback.  (do you REALIZE how much memory all the messages would take?!?)
227
		// This will be called from the template.
228
		$bodyParser = new Normal([], false);
229 2
		$opt = new ValuesContainer([
230
			'icon_sources' => new MessageTopicIcons(!empty($modSettings['messageIconChecks_enable']), $settings['theme_dir']),
231
			'show_signatures' => $this->_show_signatures,
232
		]);
233 2
		$renderer = new DisplayRenderer($messages_request, $this->user, $bodyParser, $opt);
234
		$context['get_message'] = [$renderer, 'getContext'];
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 = [
241
			'id' => 'message',
242
			'value' => '',
243
			'labels' => [
244 2
				'post_button' => $txt['post'],
245
			],
246
			// add height and width for the editor
247 2
			'height' => '250px',
248
			'width' => '100%',
249
			'smiley_container' => 'smileyBox_message',
250 2
			'bbc_container' => 'bbcBox_message',
251
			// We submit/switch to the full post page for the preview
252
			'preview_type' => 1,
253
			'buttons' => [
254
				'more' => [
255
					'type' => 'submit',
256
					'name' => 'more_options',
257
					'value' => $txt['post_options'],
258 2
					'options' => ''
259
				]
260
			],
261
		];
262
263 2
		// Load the template basics now as template_layers is requested by the prepare_context event
264
		theme()->getTemplates()->load('Display');
265
		$this->_template_layers = theme()->getLayers();
266
		$this->_template_layers->addEnd('messages_informations');
267
		$context['sub_template'] = 'messages';
268
269
		// Trigger the prepare_context event for modules that have tied in to it
270
		$this->_events->trigger('prepare_context', ['editorOptions' => &$editorOptions, 'use_quick_reply' => !empty($options['display_quick_reply'])]);
271
272
		// Load up the "double post" sequencing magic.
273
		if (!empty($options['display_quick_reply']))
274
		{
275
			checkSubmitOnce('register');
276
			$context['name'] = $_SESSION['guest_name'] ?? '';
277
			$context['email'] = $_SESSION['guest_email'] ?? '';
278 2
			if ($context['can_reply'])
279
			{
280 2
				// Needed for the editor and message icons.
281 2
				require_once(SUBSDIR . '/Editor.subs.php');
282
				create_control_richedit($editorOptions);
283
			}
284
		}
285 2
286
		theme()->addJavascriptVar(['notification_topic_notice' => $context['is_marked_notify'] ? $txt['notification_disable_topic'] : $txt['notification_enable_topic']], true);
287 2
288
		// Build the common to all buttons like Reply Notify Mark...
289
		$this->buildNormalButtons();
290
291
		// Build specialized buttons, like moderation
292
		$this->buildModerationButtons();
293
294
		// Let's get nosey, who is viewing this topic?
295
		if (!empty($settings['display_who_viewing']))
296 2
		{
297
			require_once(SUBSDIR . '/Who.subs.php');
298
			formatViewers($this->topicinfo['id_topic'], 'topic');
299
		}
300
301 2
		// Did we report a post to a moderator just now?
302
		if ($this->_req->hasQuery('reportsent'))
303
		{
304 2
			$this->_template_layers->add('report_sent');
305 2
		}
306
307
		// All of our buttons and bottom navigation
308
		$this->_template_layers->add('moderation_buttons');
309
		$this->_template_layers->add('pages_and_buttons');
310 2
311 2
		// Quick reply & modify enabled?
312
		if ($context['can_reply'] && !empty($options['display_quick_reply']))
313
		{
314
			loadJavascriptFile(['editor/mentioning.js', 'quickQuote.js'], ['defer' => true]);
315
			$this->_template_layers->addBefore('quickreply', 'moderation_buttons');
316
			theme()->addInlineJavascript("
317 2
				document.addEventListener('DOMContentLoaded', () => new Elk_QuickQuote(), false);", true
318 2
			);
319
		}
320
	}
321 2
322 2
	/**
323
	 * Sets the message per page
324
	 */
325 2
	public function setMessagesPerPage(): void
326
	{
327
		global $modSettings, $options;
328 2
329
		$this->messages_per_page = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? (int) $options['messages_per_page'] : (int) $modSettings['defaultMaxMessages'];
330
	}
331 2
332 2
	/**
333
	 * Returns IF we are counting unapproved posts
334
	 *
335
	 * @return bool
336
	 */
337
	public function getIncludeUnapproved(): bool
338 2
	{
339
		global $modSettings;
340 2
341 2
		return !$modSettings['postmod_active'] || allowedTo('approve_posts');
342
	}
343
344
	/**
345 2
	 * Return if we allow showing ALL messages for a topic vs. pagination
346 2
	 *
347
	 * @param int $total_visible_posts
348
	 * @return bool
349
	 */
350
	public function getCanShowAll($total_visible_posts): bool
351 2
	{
352
		global $modSettings;
353
354
		return !empty($modSettings['enableAllMessages'])
355
			&& $total_visible_posts > $this->messages_per_page
356
			&& $total_visible_posts < $modSettings['enableAllMessages'];
357 2
	}
358 2
359
	/**
360
	 * If show all is requested and allowed, setup to do just that
361
	 *
362 2
	 * @param bool $can_show_all
363 2
	 * @param int $total_visible_posts
364 2
	 * @return void
365
	 */
366
	public function setupShowAll($can_show_all, $total_visible_posts): void
367
	{
368
		global $scripturl, $topic, $context, $url_format;
369 2
370 2
		$all_requested = $this->_req->getQuery('all', 'trim');
371
		if (isset($all_requested))
372
		{
373
			// If all is set, but not allowed... just unset it.
374 2
			if (!$can_show_all)
375
			{
376
				unset($all_requested);
377
			}
378
			else
379
			{
380
				// Otherwise, it must be allowed... so pretend start was -1.
381
				$this->_start = -1;
382
			}
383
		}
384 2
385 2
		// Construct the page index, allowing for the .START method...
386 2
		if ($url_format === 'queryless')
387
		{
388
			$context['page_index'] = constructPageIndex($scripturl . '?topic,' . $topic . '.%1$d.html', $this->_start, $total_visible_posts, $this->messages_per_page, true, ['all' => $can_show_all, 'all_selected' => isset($all_requested)]);
389
		}
390 2
		else
391 2
		{
392
			$base_url = getUrl('topic', ['topic' => $topic, 'subject' => $this->topicinfo['subject']]);
393
			$base_url = str_replace('%', '%%', $base_url);
394 2
			$context['page_index'] = constructPageIndex($base_url . '.%1$d', $this->_start, $total_visible_posts, $this->messages_per_page, true, ['all' => $can_show_all, 'all_selected' => isset($all_requested)]);
395 2
		}
396 2
		$context['start'] = $this->_start;
397 2
398 2
		// Figure out all the link to the next/prev
399
		$context['links'] += [
400 2
			'prev' => $this->_start >= $this->messages_per_page ? $scripturl . '?topic=' . $topic . '.' . ($this->_start - $this->messages_per_page) : '',
401
			'next' => $this->_start + $this->messages_per_page < $total_visible_posts ? $scripturl . '?topic=' . $topic . '.' . ($this->_start + $this->messages_per_page) : '',
402
		];
403 2
404 2
		// If they are viewing all the posts, show all the posts, otherwise limit the number.
405 2
		if ($can_show_all && isset($all_requested))
406 2
		{
407
			// No limit! (actually, there is a limit, but...)
408
			$this->messages_per_page = -1;
409 2
410
			// Set the start back to 0...
411
			$this->_start = 0;
412 2
		}
413
	}
414
415 2
	/**
416 2
	 * Returns the previous or next topic based on the get/query value
417 2
	 * @return void
418 2
	 */
419 2
	public function getPreviousNextTopic(): void
420
	{
421
		global $board_info, $topic, $board, $context;
422
423
		$prev_next = $this->_req->getQuery('prev_next', 'trim');
424
425
		// Find the previous or next topic.  Make a fuss if there are no more.
426
		if ($prev_next === 'prev' || $prev_next === 'next')
427
		{
428
			// No use in calculating the next topic if there's only one.
429 2
			if ($board_info['num_topics'] > 1)
430 2
			{
431 2
				$topic = $prev_next === 'prev'
432
					? previousTopic($topic, $board, $this->user->id, $this->getIncludeUnapproved())
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
433
					: nextTopic($topic, $board, $this->user->id, $this->getIncludeUnapproved());
434
435 2
				$context['current_topic'] = $topic;
436 2
			}
437 2
438 2
			// Go to the newest message on this topic.
439 2
			$this->_start = 'new';
440
		}
441 2
	}
442
443
	/**
444 2
	 * Add one to the stats
445
	 * @param $topic
446 2
	 */
447
	public function increaseTopicViews($topic): void
448 2
	{
449 2
		if ($this->user->possibly_robot === false
0 ignored issues
show
Bug Best Practice introduced by
The property possibly_robot does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
450
			&& (empty($_SESSION['last_read_topic']) || $_SESSION['last_read_topic'] !== $topic))
451 2
		{
452
			increaseViewCounter($topic);
453 2
			$_SESSION['last_read_topic'] = $topic;
454
		}
455 2
	}
456 2
457
	/**
458 2
	 * Fetch all the topic information.  Provides addons a hook to add additional tables/selects
459
	 *
460 2
	 * @param int $topic
461
	 * @param int $board
462
	 * @throws Exception on invalid topic value
463
	 */
464 2
	public function loadTopicInfo($topic, $board): void
465
	{
466
		$topic_selects = [];
467 2
		$topic_tables = [];
468
		$topic_parameters = [
469 2
			'topic' => $topic,
470 2
			'member' => $this->user->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
471
			'board' => (int) $board,
472
		];
473
474 2
		// Allow addons to add additional details to the topic query
475
		call_integration_hook('integrate_topic_query', [&$topic_selects, &$topic_tables, &$topic_parameters]);
476
477 2
		// Load the topic details
478
		$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...
479 2
480
		// Nothing??
481
		if (empty($this->topicinfo))
482 2
		{
483
			throw new Exception('not_a_topic', false);
484
		}
485
	}
486
487
	/**
488
	 * Sometimes topics have been moved, this will direct the user to the right spot
489 2
	 */
490
	public function handleRedirection(): void
491
	{
492
		global $context;
493 2
494 2
		// Need to send the user to the new location?
495
		if (!empty($this->topicinfo['id_redirect_topic']) && !$this->_req->hasQuery('noredir'))
496 2
		{
497 2
			markTopicsRead([$this->user->id, $this->topicinfo['id_topic'], $this->topicinfo['id_last_msg'], 0], $this->topicinfo['new_from'] !== 0);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
498 2
			redirectexit('topic=' . $this->topicinfo['id_redirect_topic'] . '.0;redirfrom=' . $this->topicinfo['id_topic']);
499
		}
500
501 2
		// Or are we here because we were redirected?
502
		if ($this->_req->hasQuery('redirfrom'))
503
		{
504
			$redirfrom = $this->_req->getQuery('redirfrom', 'intval');
505
			$redir_topics = topicsList([$redirfrom]);
506
			if (!empty($redir_topics[$redirfrom]))
507 2
			{
508
				$context['topic_redirected_from'] = $redir_topics[$redirfrom];
509 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']);
510 2
			}
511
		}
512
	}
513 2
514 2
	/**
515 2
	 * Number of posts that this user can see.  Will include unapproved for those with access
516
	 *
517 2
	 * @param int $num_replies
518
	 * @return int
519
	 */
520 2
	public function getVisiblePosts($num_replies): int
521
	{
522
		if (!$this->includeUnapproved && $this->topicinfo['unapproved_posts'] && $this->user->is_guest === false)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
523
		{
524 2
			$myUnapprovedPosts = unapprovedPosts($this->topicinfo['id_topic'], $this->user->id);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
525 2
526 2
			return $num_replies + $myUnapprovedPosts + ($this->topicinfo['approved'] ? 1 : 0);
527
		}
528
529
		if ($this->user->is_guest)
530
		{
531
			return $num_replies + ($this->topicinfo['approved'] ? 1 : 0);
532
		}
533
534
		return $num_replies + $this->topicinfo['unapproved_posts'] + ($this->topicinfo['approved'] ? 1 : 0);
535
	}
536
537 2
	/**
538
	 * The start value from get can contain all manners of information on what to do.
539
	 * This converts new, from, msg into something useful, most times.
540 2
	 *
541
	 * @param int $total_visible_posts
542
	 */
543 2
	public function makeStartAdjustments($total_visible_posts): void
544
	{
545 2
		global $modSettings;
546
547
		$start = $this->_start;
548
		if (!is_numeric($start))
549 2
		{
550 2
			// Redirect to the page and post with new messages
551
			if ($start === 'new')
552
			{
553
				// Guests automatically go to the last post.
554
				$start = $this->user->is_guest ? $total_visible_posts - 1 : 'msg' . $this->topicinfo['new_from'];
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
555
			}
556
557
			// Start from a certain time index, not a message.
558
			if (str_starts_with($start, 'from'))
559 2
			{
560 2
				$timestamp = (int) substr($start, 4);
561 2
				$start = $timestamp === 0 ? 0 : countNewPosts($this->topicinfo['id_topic'], $this->topicinfo, $timestamp);
562 2
			}
563
			elseif (str_starts_with($start, 'msg'))
564 2
			{
565 2
				$this->_virtual_msg = (int) substr($start, 3);
566
				if (!$this->topicinfo['unapproved_posts'] && $this->_virtual_msg >= $this->topicinfo['id_last_msg'])
567 2
				{
568 2
					$start = $total_visible_posts - 1;
569
				}
570
				elseif (!$this->topicinfo['unapproved_posts'] && $this->_virtual_msg <= $this->topicinfo['id_first_msg'])
571
				{
572
					$start = 0;
573
				}
574 2
				else
575
				{
576
					$only_approved = $modSettings['postmod_active'] && $this->topicinfo['unapproved_posts'] && !allowedTo('approve_posts');
577
					$start = countMessagesBefore($this->topicinfo['id_topic'], $this->_virtual_msg, false, $only_approved, $this->user->is_guest === false);
578
				}
579 2
			}
580 2
		}
581 2
582 2
		$this->start_from = $start;
0 ignored issues
show
Documentation Bug introduced by
It seems like $start can also be of type string. However, the property $start_from is declared as type integer. 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...
583
		$this->_start = $start;
584 2
	}
585
586 2
	/**
587
	 * Sets all we know about a message into $context for template consumption.
588
	 * Note: After this processes, some amount of additional context is still added, read
589
	 * the code.
590 2
	 */
591
	public function setMessageContext(): void
592
	{
593
		global $context, $modSettings, $txt, $board_info;
594
595
		// Going to allow this to be indexed by Mr. Robot?
596
		$context['robot_no_index'] = $this->setRobotNoIndex();
597
598
		// Some basics for the template
599
		$context['num_replies'] = $this->topicinfo['num_replies'];
600
		$context['topic_first_message'] = $this->topicinfo['id_first_msg'];
601
		$context['topic_last_message'] = $this->topicinfo['id_last_msg'];
602
		$context['topic_unwatched'] = $this->topicinfo['unwatched'] ?? 0;
603
		$context['start_from'] = $this->start_from;
604
		$context['topic_start_time'] = htmlTime($this->topicinfo['poster_time']);
605 2
		$context['topic_starter_name'] = $this->topicinfo['poster_name'];
606
607 2
		// Did this user start the topic or not?
608
		$context['user']['started'] = $this->didThisUserStart();
609
		$context['topic_starter_id'] = $this->topicinfo['id_member_started'];
610
611
		// Add up unapproved replies to get the real number of replies...
612 2
		$context['real_num_replies'] = $this->topicinfo['num_replies'];
613
		if ($modSettings['postmod_active'] && allowedTo('approve_posts'))
614
		{
615
			$context['real_num_replies'] += $this->topicinfo['unapproved_posts'] - ($this->topicinfo['approved'] ? 0 : 1);
616
		}
617
618 2
		// When was the last time this topic was replied to?  Should we warn them about it?
619
		$context['oldTopicError'] = $this->warnOldTopic();
620 2
621
		// Are we showing signatures - or disabled fields?
622
		$context['signature_enabled'] = str_starts_with($modSettings['signature_settings'], '1');
623
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : [];
624 2
625 2
		// Page title & description
626 2
		$context['page_title'] = $this->topicinfo['subject'];
627 2
		$context['page_description'] = Util::shorten_text(strip_tags($this->topicinfo['body']), 128);
628
629
		// Create a previous next string if the selected theme has it as a selected option.
630 2
		if ($modSettings['enablePreviousNext'])
631
		{
632
			$context['links'] += [
633 2
				'go_prev' => getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => '0', 'subject' => $this->topicinfo['subject'], 'prev_next' => 'prev']) . '#new',
634
				'go_next' => getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => '0', 'subject' => $this->topicinfo['subject'], 'prev_next' => 'next']) . '#new'
635
			];
636
		}
637
638 2
		// Build the jump to box
639 2
		$context['jump_to'] = [
640 2
			'label' => addslashes(un_htmlspecialchars($txt['jump_to'])),
641 2
			'board_name' => htmlspecialchars(strtr(strip_tags($board_info['name']), ['&amp;' => '&']), ENT_COMPAT),
642 2
			'child_level' => $board_info['child_level'],
643 2
		];
644
645
		// Build a list of this board's moderators.
646 2
		$context['moderators'] = &$board_info['moderators'];
647
		$context['link_moderators'] = [];
648
649 2
		// Information about the current topic...
650 2
		$context['is_locked'] = $this->topicinfo['locked'];
651
		$context['is_sticky'] = $this->topicinfo['is_sticky'];
652
		$context['is_very_hot'] = $this->topicinfo['num_replies'] >= $modSettings['hotTopicVeryPosts'];
653 2
		$context['is_hot'] = $this->topicinfo['num_replies'] >= $modSettings['hotTopicPosts'];
654
		$context['is_approved'] = $this->topicinfo['approved'];
655
656 2
		// Set the class of the current topic, Hot, not so hot, locked, sticky
657
		determineTopicClass($context);
658
659
		// Set the topic's information for the template.
660
		$context['subject'] = $this->topicinfo['subject'];
661
		$context['num_views'] = $this->topicinfo['num_views'];
662
		$context['num_views_text'] = (int) $this->topicinfo['num_views'] === 1 ? $txt['read_one_time'] : sprintf($txt['read_many_times'], $this->topicinfo['num_views']);
663
		$context['mark_unread_time'] = empty($this->_virtual_msg) ? $this->topicinfo['new_from'] : $this->_virtual_msg;
664
665
		// Set a canonical URL for this page.
666 2
		$context['canonical_url'] = getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => $context['start'], 'subject' => $this->topicinfo['subject']]);
667 2
668
		// For quick reply we need a response prefix in the default forum language.
669 2
		$context['response_prefix'] = response_prefix();
670
671
		$context['messages_per_page'] = $this->messages_per_page;
672 2
673 2
		// Build the link tree.
674
		$context['breadcrumbs'][] = [
675 2
			'url' => getUrl('topic', ['topic' => $this->topicinfo['id_topic'], 'start' => '0', 'subject' => $this->topicinfo['subject']]),
676
			'name' => $this->topicinfo['subject'],
677
		];
678
	}
679 2
680
	/**
681
	 * Sets if this is a page that we do or do not want bots to index
682 2
	 *
683
	 * @return bool
684 2
	 */
685 2
	public function setRobotNoIndex(): bool
686 2
	{
687 2
		// Let's do some work on what to search index.
688
		if (count((array) $this->_req->query) > 2)
689
		{
690
			foreach (['topic', 'board', 'start', session_name()] as $key)
691
			{
692
				if (!isset($this->_req->query->$key))
693
				{
694
					return true;
695
				}
696 2
			}
697
		}
698 2
699
		return !empty($this->_start)
700 2
			&& (!is_numeric($this->_start) || $this->_start % $this->messages_per_page !== 0);
701 2
	}
702 2
703 2
	/**
704 2
	 * Return if the current user started this topic, as that may provide them additional permissions.
705 2
	 *
706
	 * @return bool
707
	 */
708
	public function didThisUserStart(): bool
709 2
	{
710 1
		return ((int) $this->user->id === (int) $this->topicinfo['id_member_started']) && !$this->user->is_guest;
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
711 2
	}
712 2
713 2
	/**
714
	 * They Hey bub, what's with the necro-bump message
715 2
	 *
716
	 * @return bool
717
	 */
718
	public function warnOldTopic(): bool
719 2
	{
720 2
		global $modSettings, $board_info;
721 2
722
		if (!empty($modSettings['oldTopicDays']) && !empty($board_info['old_posts']))
723 2
		{
724 2
			$mgsOptions = basicMessageInfo($this->topicinfo['id_last_msg'], true);
725
726
			// May return nothing if not approved
727 2
			if ($mgsOptions !== false)
728 2
			{
729 2
				return $mgsOptions['poster_time'] + $modSettings['oldTopicDays'] * 86400 < time()
730
					&& empty($this->topicinfo['is_sticky']);
731 2
			}
732
		}
733
734 2
		return false;
735 2
	}
736 2
737
	/**
738 2
	 * Marks notifications read for specified messages
739 2
	 *
740
	 * @param array $messages An array of message ids
741
	 * @return void
742 2
	 */
743 2
	private function markNotificationsRead($messages): void
744 2
	{
745
		global $modSettings;
746 2
747 2
		$mark_at_msg = max($messages);
748
		if ($mark_at_msg >= $this->topicinfo['id_last_msg'])
749
		{
750 2
			$mark_at_msg = $modSettings['maxMsgID'];
751 2
		}
752 2
753
		// If there are new messages "in view", let's mark any notification for them as read
754 2
		if ($mark_at_msg >= $this->topicinfo['new_from'])
755 2
		{
756 2
			require_once(SUBSDIR . '/Mentions.subs.php');
757
			$filterMessages = array_filter($messages, static function ($element) use ($mark_at_msg) {
758
				return $element >= $mark_at_msg;
759
			});
760
761 2
			markNotificationsRead($filterMessages);
762 1
		}
763 2
	}
764 2
765 2
	/**
766
	 * Keeps track of where the user is in reading this topic.
767 2
	 *
768
	 * @param array $messages
769
	 * @param int $board
770 2
	 */
771 2
	private function markRead($messages, $board): void
772 2
	{
773
		global $modSettings;
774 2
775 2
		// Guests can't mark topics read or for notifications, just can't, sorry.
776
		if ($this->user->is_guest || empty($messages))
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
777
		{
778 2
			return;
779 2
		}
780 2
781
		$boardseen = isset($this->_req->query->boardseen);
782 2
783
		$mark_at_msg = max($messages);
784
		if ($mark_at_msg >= $this->topicinfo['id_last_msg'])
785 2
		{
786 2
			$mark_at_msg = $modSettings['maxMsgID'];
787 2
		}
788
789 2
		if ($mark_at_msg >= $this->topicinfo['new_from'])
790
		{
791
			markTopicsRead([$this->user->id, $this->topicinfo['id_topic'], $mark_at_msg, $this->topicinfo['unwatched']], $this->topicinfo['new_from'] !== 0);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
792 2
			$numNewTopics = getUnreadCountSince($board, empty($_SESSION['id_msg_last_visit']) ? 0 : $_SESSION['id_msg_last_visit']);
793 2
794 2
			if (empty($numNewTopics))
795
			{
796 2
				$boardseen = true;
797
			}
798
		}
799
800
		updateReadNotificationsFor($this->topicinfo['id_topic'], $board);
801 2
802
		// Mark the board as seen if we came using the last post link from BoardIndex. (or other places...)
803
		if ($boardseen)
804
		{
805
			require_once(SUBSDIR . '/Boards.subs.php');
806
			markBoardsRead($board);
807
		}
808
	}
809
810
	/**
811
	 * If the QR is on, we need to load the user information into $context, so we
812 2
	 * can show the new improved 2.0 QR area
813
	 */
814 2
	public function prepareQuickReply(): void
815
	{
816
		global $options, $context;
817 2
818
		if (empty($options['hide_poster_area']) && $options['display_quick_reply'])
819
		{
820 2
			if (User::$info->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
821 2
			{
822 2
				MembersList::loadGuest();
823
			}
824
825
			// First, let's load the profile array
826
			$thisUser = MembersList::get(User::$info->id);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
827
			$thisUser->loadContext();
828
			$context['thisMember'] = [
829
				'id' => 'new',
830
				'is_message_author' => true,
831
				'member' => $thisUser->toArray()['data']
832
			];
833
		}
834
	}
835
836
	/**
837
	 * Sets if we are showing signatures or not
838
	 */
839
	public function setSignatureShowStatus(): void
840
	{
841
		global $modSettings;
842
843
		[$sig_limits] = explode(':', $modSettings['signature_settings']);
844
		$signature_settings = explode(',', $sig_limits);
845
		if ($this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
846
		{
847
			$this->_show_signatures = empty($signature_settings[8]) ? 0 : (int) $signature_settings[8];
848
		}
849
		else
850
		{
851
			$this->_show_signatures = empty($signature_settings[9]) ? 0 : (int) $signature_settings[9];
852
		}
853
	}
854
855
	/**
856
	 * Loads into context the various message/topic permissions so the template
857
	 * knows what buttons etc. to show
858
	 */
859
	public function setTopicCanPermissions(): void
860
	{
861
		global $modSettings, $context, $settings, $board;
862
863
		// First the common ones
864
		$common_permissions = [
865
			'can_approve' => 'approve_posts',
866
			'can_ban' => 'manage_bans',
867
			'can_sticky' => 'make_sticky',
868
			'can_merge' => 'merge_any',
869
			'can_split' => 'split_any',
870
			'can_mark_notify' => 'mark_any_notify',
871
			'can_send_pm' => 'pm_send',
872
			'can_send_email' => 'send_email_to_members',
873
			'can_report_moderator' => 'report_any',
874
			'can_moderate_forum' => 'moderate_forum',
875
			'can_issue_warning' => 'issue_warning',
876
			'can_restore_topic' => 'move_any',
877
			'can_restore_msg' => 'move_any',
878
		];
879
		foreach ($common_permissions as $contextual => $perm)
880
		{
881
			$context[$contextual] = allowedTo($perm);
882
		}
883
884
		// Permissions with _any/_own versions.  $context[YYY] => ZZZ_any/_own.
885
		$anyown_permissions = [
886
			'can_move' => 'move',
887
			'can_lock' => 'lock',
888
			'can_delete' => 'remove',
889
			'can_reply' => 'post_reply',
890
			'can_reply_unapproved' => 'post_unapproved_replies',
891
		];
892
		foreach ($anyown_permissions as $contextual => $perm)
893
		{
894
			$context[$contextual] = allowedTo($perm . '_any') || ($this->didThisUserStart() && allowedTo($perm . '_own'));
895
		}
896
897
		// Clean up all the permissions with extra stuff...
898
		$context['can_mark_notify'] = $context['can_mark_notify'] && !$context['user']['is_guest'];
899
		$context['can_reply'] = $context['can_reply'] && (empty($this->topicinfo['locked']) || allowedTo('moderate_board'));
900
		$context['can_reply_unapproved'] = $context['can_reply_unapproved'] && $modSettings['postmod_active'] && (empty($this->topicinfo['locked']) || allowedTo('moderate_board'));
901
		$context['can_issue_warning'] = $context['can_issue_warning'] && featureEnabled('w') && !empty($modSettings['warning_enable']);
902
903
		// Handle approval flags...
904
		$context['can_reply_approved'] = $context['can_reply'];
905
906
		// Guests do not have post_unapproved_replies_own permission, so it's always post_unapproved_replies_any
907
		if ($this->user->is_guest && allowedTo('post_unapproved_replies_any'))
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
908
		{
909
			$context['can_reply_approved'] = false;
910
		}
911
912
		$context['can_reply'] = $context['can_reply'] || $context['can_reply_unapproved'];
913
		$context['can_quote'] = $context['can_reply'] && (empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']), true));
914
		$context['can_mark_unread'] = $this->user->is_guest === false && $settings['show_mark_read'];
915
		$context['can_unwatch'] = $this->user->is_guest === false && $modSettings['enable_unwatch'];
916
		$context['can_print'] = empty($modSettings['disable_print_topic']);
917
918
		// Start this off for quick moderation - it will be or'd for each post.
919
		$context['can_remove_post'] = allowedTo('delete_any') || (allowedTo('delete_replies') && $this->didThisUserStart());
920
921
		// Can restore a topic?  That's if the topic is in the recycle board and has a previous restore state.
922
		$context['can_restore_topic'] = $context['can_restore_topic'] && !empty($modSettings['recycle_enable']) && (int) $modSettings['recycle_board'] === $board && !empty($this->topicinfo['id_previous_board']);
923
		$context['can_restore_msg'] = $context['can_restore_msg'] && !empty($modSettings['recycle_enable']) && (int) $modSettings['recycle_board'] === $board && !empty($this->topicinfo['id_previous_topic']);
924
	}
925
926
	/**
927
	 * Loads into $context the normal button array for template use.
928
	 * Calls integrate_display_buttons hook
929
	 */
930
	public function buildNormalButtons(): void
931
	{
932
		global $context, $txt;
933
934
		// Build the normal button array.
935
		$context['normal_buttons'] = [
936
			'reply' => [
937
				'test' => 'can_reply',
938
				'text' => 'reply',
939
				'lang' => true,
940
				'url' => getUrl('action', ['action' => 'post', 'topic' => $context['current_topic'] . '.' . $context['start'], 'last_msg' => $this->topicinfo['id_last_msg']]),
941
				'active' => true,
942
			],
943
			'notify' => [
944
				'test' => 'can_mark_notify',
945
				'text' => $context['is_marked_notify'] ? 'unnotify' : 'notify',
946
				'lang' => true,
947
				'custom' => 'onclick="return notifyButton(this);"',
948
				'url' => getUrl('action', ['action' => 'notify', 'sa' => $context['is_marked_notify'] ? 'off' : 'on', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}']),
949
			],
950
			'mark_unread' => [
951
				'test' => 'can_mark_unread',
952
				'text' => 'mark_unread',
953
				'lang' => true,
954
				'url' => getUrl('action', ['action' => 'markasread', 'sa' => 'topic', 't' => $context['mark_unread_time'], 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}']),
955
			],
956
			'unwatch' => [
957
				'test' => 'can_unwatch',
958
				'text' => ($context['topic_unwatched'] ? '' : 'un') . 'watch',
959
				'lang' => true,
960
				'custom' => 'onclick="return unwatchButton(this);"',
961
				'url' => getUrl('action', ['action' => 'unwatchtopic', 'sa' => $context['topic_unwatched'] ? 'off' : 'on', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}']),
962
				'submenu' => true,
963
			],
964
			'print' => [
965
				'test' => 'can_print',
966
				'text' => 'print',
967
				'lang' => true,
968
				'custom' => 'rel="nofollow"',
969
				'class' => 'new_win',
970
				'url' => getUrl('action', ['action' => 'topic', 'sa' => 'printpage', 'topic' => $context['current_topic'] . '.0']),
971
				'submenu' => true,
972
			],
973
		];
974
975
		// Allow adding new buttons easily.
976
		call_integration_hook('integrate_display_buttons');
977
	}
978
979
	/**
980
	 * Loads into $context the moderation button array for template use.
981
	 * Call integrate_mod_buttons hook
982
	 */
983
	public function buildModerationButtons(): void
984
	{
985
		global $context, $txt;
986
987
		// Build the mod button array
988
		$context['mod_buttons'] = [
989
			'move' => [
990
				'test' => 'can_move',
991
				'text' => 'move_topic',
992
				'lang' => true,
993
				'url' => getUrl('action', ['action' => 'movetopic', 'current_board' => $context['current_board'], 'topic' => $context['current_topic'] . '.0'])
994
			],
995
			'delete' => [
996
				'test' => 'can_delete',
997
				'text' => 'remove_topic',
998
				'lang' => true,
999
				'custom' => 'onclick="return confirm(\'' . $txt['are_sure_remove_topic'] . '\');"',
1000
				'url' => getUrl('action', ['action' => 'removetopic2', 'topic' => $context['current_topic'] . '.0', '{session_data}'])
1001
			],
1002
			'lock' => [
1003
				'test' => 'can_lock',
1004
				'text' => empty($this->topicinfo['locked']) ? 'set_lock' : 'set_unlock',
1005
				'lang' => true,
1006
				'url' => getUrl('action', ['action' => 'topic', 'sa' => 'lock', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
1007
			],
1008
			'sticky' => [
1009
				'test' => 'can_sticky',
1010
				'text' => empty($this->topicinfo['is_sticky']) ? 'set_sticky' : 'set_nonsticky',
1011
				'lang' => true,
1012
				'url' => getUrl('action', ['action' => 'topic', 'sa' => 'sticky', 'topic' => $context['current_topic'] . '.' . $context['start'], '{session_data}'])
1013
			],
1014
			'merge' => [
1015
				'test' => 'can_merge',
1016
				'text' => 'merge',
1017
				'lang' => true,
1018
				'url' => getUrl('action', ['action' => 'mergetopics', 'board' => $context['current_board'] . '.0', 'from' => $context['current_topic']])
1019
			],
1020
		];
1021
1022
		// Restore topic. eh?  No monkey business.
1023
		if ($context['can_restore_topic'])
1024
		{
1025
			$context['mod_buttons'][] = [
1026
				'text' => 'restore_topic',
1027
				'lang' => true,
1028
				'url' => getUrl('action', ['action' => 'restoretopic', 'topics' => $context['current_topic'], '{session_data}'])
1029
			];
1030
		}
1031
1032
		// Allow adding new buttons easily.
1033
		call_integration_hook('integrate_mod_buttons');
1034
	}
1035
1036
	/**
1037
	 * If we are on a topic and don't have permission to approve it, then duck out now.
1038
	 * This is an abuse of the method, but it's easier that way.
1039
	 *
1040
	 * @param string $action the function name of the current action
1041
	 *
1042
	 * @return bool
1043
	 * @throws Exception not_a_topic
1044
	 */
1045
	public function trackStats($action = '')
1046
	{
1047
		global $topic, $board_info;
1048
1049
		if (!empty($topic)
1050
			&& empty($board_info['cur_topic_approved'])
1051
			&& ($this->user->id !== (int) $board_info['cur_topic_starter'] || $this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1052
			&& !allowedTo('approve_posts'))
1053
		{
1054
			throw new Exception('not_a_topic', false);
1055
		}
1056
1057
		return parent::trackStats($action);
1058
	}
1059
1060
	/**
1061
	 * In-topic quick moderation.
1062
	 *
1063
	 * Accessed by ?action=quickmod2 from quickModForm
1064
	 */
1065
	public function action_quickmod2(): void
1066
	{
1067
		global $topic, $board, $context, $modSettings;
1068
1069
		// Check the session = get or post.
1070
		checkSession('request');
1071
1072
		require_once(SUBSDIR . '/Messages.subs.php');
1073
1074
		if (empty($this->_req->post->msgs))
1075
		{
1076
			redirectexit('topic=' . $topic . '.' . $this->_req->getQuery('start', 'intval'));
1077
		}
1078
1079
		$messages = array_map('intval', $this->_req->post->msgs);
1080
1081
		// We are restoring messages. We handle this in another place.
1082
		if ($this->_req->hasQuery('restore_selected'))
1083
		{
1084
			redirectexit('action=restoretopic;msgs=' . implode(',', $messages) . ';' . $context['session_var'] . '=' . $context['session_id']);
1085
		}
1086
1087
		if ($this->_req->hasQuery('split_selection'))
1088
		{
1089
			$mgsOptions = basicMessageInfo(min($messages), true);
1090
1091
			$_SESSION['split_selection'][$topic] = $messages;
1092
			redirectexit('action=splittopics;sa=selectTopics;topic=' . $topic . '.0;subname_enc=' . urlencode($mgsOptions['subject']) . ';' . $context['session_var'] . '=' . $context['session_id']);
1093
		}
1094
1095
		require_once(SUBSDIR . '/Topic.subs.php');
1096
		$topic_info = getTopicInfo($topic);
1097
1098
		// Allowed to delete any message?
1099
		$allowed_all = $this->canDeleteAll($topic_info);
1100
1101
		// Make sure they're allowed to delete their own messages, if not any.
1102
		if (!$allowed_all)
1103
		{
1104
			isAllowedTo('delete_own');
1105
		}
1106
1107
		// Allowed to remove which messages?
1108
		$messages = determineRemovableMessages($topic, $messages, $allowed_all);
1109
1110
		// Get the first message in the topic - because you can't delete that!
1111
		$first_message = (int) $topic_info['id_first_msg'];
1112
		$last_message = (int) $topic_info['id_last_msg'];
1113
		$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
1114
1115
		// Delete all the messages we know they can delete. ($messages)
1116
		foreach ($messages as $message => $info)
1117
		{
1118
			$message = (int) $message;
1119
1120
			// Just skip the first message - if it's not the last.
1121
			if ($message === $first_message && $message !== $last_message)
1122
			{
1123
				continue;
1124
			}
1125
1126
			// If the first message is going, then don't bother going back to the topic as we're effectively deleting it.
1127
			if ($message === $first_message)
1128
			{
1129
				$topicGone = true;
1130
			}
1131
1132
			$remover->removeMessage($message);
1133
		}
1134
1135
		redirectexit(empty($topicGone) ? 'topic=' . $topic . '.' . (int) $this->_req->query->start : 'board=' . $board);
1136
	}
1137
1138
	/**
1139
	 * Determine if this user can delete all replies in this message
1140
	 *
1141
	 * @param array $topic_info
1142
	 * @return bool
1143
	 */
1144
	public function canDeleteAll($topic_info): bool
1145
	{
1146
		if (allowedTo('delete_any'))
1147
		{
1148
			return true;
1149
		}
1150
1151
		// Allowed to delete replies to their messages?
1152
		if (allowedTo('delete_replies'))
1153
		{
1154
			return (int) $topic_info['id_member_started'] === (int) $this->user->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1155
		}
1156
1157
		return false;
1158
	}
1159
}
1160