ProfileInfo::_determine_warning_level()   B
last analyzed

Complexity

Conditions 7
Paths 4

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 7
nc 4
nop 0
dl 0
loc 16
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Handles the retrieving and display of a users posts, attachments, stats, permissions
5
 * warnings and the like
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
namespace ElkArte\Profile;
19
20
use BBC\ParserWrapper;
21
use ElkArte\AbstractController;
22
use ElkArte\Action;
23
use ElkArte\Exceptions\Exception;
24
use ElkArte\Helper\FileFunctions;
25
use ElkArte\Helper\Util;
26
use ElkArte\Languages\Txt;
27
use ElkArte\Member;
28
use ElkArte\MembersList;
29
use ElkArte\MessagesDelete;
30
31
/**
32
 * Access all profile summary areas for a user including overall summary,
33
 * post listing, attachment listing, user statistics user permissions, user warnings
34
 */
35
class ProfileInfo extends AbstractController
36
{
37
	/** @var int Member id for the profile being worked with */
38
	private $_memID = 0;
39
40
	/** @var Member The \ElkArte\Member object is stored here to avoid some global */
41
	private $_profile;
42
43
	/** @var array Holds the current summary tabs to load */
44
	private $_summary_areas;
45
46
	/**
47
	 * Called before all other methods when coming from the dispatcher or
48
	 * action class.
49
	 *
50
	 * - If you initiate the class outside of those methods, call this method.
51
	 * or setup the class yourself or fall awaits.
52
	 */
53
	public function pre_dispatch()
54
	{
55
		global $context;
56
57
		require_once(SUBSDIR . '/Profile.subs.php');
58
59
		$this->_memID = currentMemberID();
60
		$this->_profile = MembersList::get($this->_memID);
0 ignored issues
show
Documentation Bug introduced by
It seems like ElkArte\MembersList::get($this->_memID) can also be of type anonymous//sources/ElkArte/MembersList.php$0. However, the property $_profile is declared as type ElkArte\Member. 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...
61
62
		if (!isset($context['user']['is_owner']))
63
		{
64
			$context['user']['is_owner'] = $this->_memID === (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...
65
		}
66
67
		// Attempt to load the member's profile data.
68
		if ($this->_profile->isEmpty())
69
		{
70
			throw new Exception('not_a_user', false);
71
		}
72
73
		$this->_profile->loadContext();
74
75
		Txt::load('Profile');
76
	}
77
78
	/**
79
	 * Intended as entry point which delegates to methods in this class...
80
	 *
81
	 * - But here, today, for now, the methods are mainly called from other places
82
	 * like menu picks and the like.
83
	 */
84
	public function action_index()
85
	{
86
		global $context;
87
88
		// What do we do, do you even know what you do?
89
		$subActions = [
90
			'buddies' => [$this, 'action_profile_buddies'],
91
			'recent' => [$this, 'action_profile_recent'],
92
			'summary' => ['controller' => Profile::class, 'function' => 'action_index'],
93
		];
94
95
		// Action control
96
		$action = new Action('profile_info');
97
98
		// By default we want the summary
99
		$subAction = $action->initialize($subActions, 'summary');
100
101
		// Final bits
102
		$context['sub_action'] = $subAction;
103
104
		// Call the right function for this sub-action.
105
		$action->dispatch($subAction);
106
	}
107
108
	/**
109
	 * View the user profile summary.
110
	 *
111
	 * @uses ProfileInfo template
112
	 */
113
	public function action_summary()
114
	{
115
		global $context, $modSettings;
116
117
		// To make tabs work, we need jQueryUI
118
		$modSettings['jquery_include_ui'] = true;
119
		$context['start_tabs'] = true;
120
		loadCSSFile('jquery.ui.tabs.css');
121
122
		theme()->getTemplates()->load('ProfileInfo');
123
		Txt::load('Profile');
124
125
		// Set a canonical URL for this page.
126
		$context['canonical_url'] = getUrl('action', ['action' => 'profile', 'u' => $this->_memID]);
127
128
		// Are there things we don't show?
129
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : [];
130
131
		// Disable Menu tab
132
		$context[$context['profile_menu_name']]['tab_data'] = [];
133
134
		// Profile summary tabs, like Summary, Recent, Buddies
135
		$this->_register_summarytabs();
136
137
		// Load in everything we know about the user to preload the summary tab
138
		$this->_define_user_values();
139
		$this->_load_summary();
140
141
		// To finish this off, custom profile fields.
142
		$profileFields = new ProfileFields();
143
		$profileFields->loadCustomFields($this->_memID);
144
	}
145
146
	/**
147
	 * Prepares the tabs for the profile summary page
148
	 *
149
	 * What it does:
150
	 *
151
	 * - Tab information for use in the summary page
152
	 * - Each tab template defines a div, the value of which are the template(s) to load in that div
153
	 * - array(array(1, 2), array(3, 4)) <div>template 1, template 2</div><div>template 3 template 4</div>
154
	 * - Templates are named template_profile_block_YOURNAME
155
	 * - Tabs with href defined will not preload/create any page divs but instead be loaded via ajax
156
	 */
157
	private function _register_summarytabs()
158
	{
159
		global $txt, $context, $modSettings;
160
161
		$context['summarytabs'] = [
162
			'summary' => [
163
				'name' => $txt['summary'],
164
				'templates' => [
165
					['summary', 'user_info'],
166
					['contact', 'other_info'],
167
					['user_customprofileinfo', 'moderation'],
168
				],
169
				'active' => true,
170
			],
171
			'recent' => [
172
				'name' => $txt['profile_recent_activity'],
173
				'templates' => ['posts', 'topics', 'attachments'],
174
				'active' => true,
175
				'href' => getUrl('action', ['action' => 'profileinfo', 'sa' => 'recent', 'api' => 'html', 'u' => $this->_memID, '{session_data}']),
176
			],
177
			'buddies' => [
178
				'name' => $txt['buddies'],
179
				'templates' => ['buddies'],
180
				'active' => !empty($modSettings['enable_buddylist']) && $context['user']['is_owner'],
181
				'href' => getUrl('action', ['action' => 'profileinfo', 'sa' => 'buddies', 'api' => 'html', 'u' => $this->_memID, '{session_data}']),
182
			]
183
		];
184
185
		// Let addons add or remove to the tabs array
186
		call_integration_hook('integrate_profile_summary', [$this->_memID]);
187
188
		// Go forward with whats left after integration adds or removes
189
		$summary_areas = '';
190
		foreach ($context['summarytabs'] as $id => $tab)
191
		{
192
			// If the tab is active we add it
193
			if (!$tab['active'])
194
			{
195
				unset($context['summarytabs'][$id]);
196
			}
197
			else
198
			{
199
				// All the active templates, used to prevent processing data we don't need
200
				foreach ($tab['templates'] as $template)
201
				{
202
					$summary_areas .= is_array($template) ? implode(',', $template) : ',' . $template;
203
				}
204
			}
205
		}
206
207
		$this->_summary_areas = explode(',', $summary_areas);
208
	}
209
210
	/**
211
	 * Sets in to context what we know about a given user
212
	 *
213
	 * - Defines various user permissions for profile views
214
	 */
215
	private function _define_user_values()
216
	{
217
		global $context, $modSettings, $txt;
218
219
		// Set up the context stuff and load the user.
220
		$context += [
221
			'page_title' => sprintf($txt['profile_of_username'], $this->_profile['name']),
222
			'can_send_pm' => allowedTo('pm_send'),
223
			'can_send_email' => allowedTo('send_email_to_members'),
224
			'can_have_buddy' => allowedTo('profile_identity_own') && !empty($modSettings['enable_buddylist']),
225
			'can_issue_warning' => featureEnabled('w') && allowedTo('issue_warning') && !empty($modSettings['warning_enable']),
226
			'can_view_warning' => featureEnabled('w') && ((allowedTo('issue_warning') && !$context['user']['is_owner']) || (!empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $context['user']['is_owner'])))
227
		];
228
229
		// @critical: potential problem here
230
		$context['member'] = $this->_profile;
231
		$context['member']->loadContext();
232
		$context['member']['id'] = $this->_memID;
233
234
		// Is the signature even enabled on this forum?
235
		$context['signature_enabled'] = strpos($modSettings['signature_settings'], "1") === 0;
236
	}
237
238
	/**
239
	 * Loads the information needed to create the profile summary view
240
	 */
241
	private function _load_summary()
242
	{
243
		// Load all areas of interest in to context for template use
244
		$this->_determine_warning_level();
245
		$this->_determine_posts_per_day();
246
		$this->_determine_age_birth();
247
		$this->_determine_member_ip();
248
		$this->_determine_member_action();
249
		$this->_determine_member_activation();
250
		$this->_determine_member_bans();
251
	}
252
253
	/**
254
	 * If they have been disciplined, show the warning level for those that can see it.
255
	 */
256
	private function _determine_warning_level()
257
	{
258
		global $modSettings, $context, $txt;
259
260
		// See if they have broken any warning levels...
261
		if (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $context['member']['warning'])
262
		{
263
			$context['warning_status'] = $txt['profile_warning_is_muted'];
264
		}
265
		elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $context['member']['warning'])
266
		{
267
			$context['warning_status'] = $txt['profile_warning_is_moderation'];
268
		}
269
		elseif (!empty($modSettings['warning_watch']) && $modSettings['warning_watch'] <= $context['member']['warning'])
270
		{
271
			$context['warning_status'] = $txt['profile_warning_is_watch'];
272
		}
273
	}
274
275
	/**
276
	 * Gives their spam level as a posts per day kind of statistic
277
	 */
278
	private function _determine_posts_per_day()
279
	{
280
		global $context, $txt;
281
282
		// They haven't even been registered for a full day!?
283
		$days_registered = (int) ((time() - $this->_profile['registered_raw']) / (3600 * 24));
284
		if (empty($this->_profile['date_registered']) || $days_registered < 1)
285
		{
286
			$context['member']['posts_per_day'] = $txt['not_applicable'];
287
		}
288
		else
289
		{
290
			$context['member']['posts_per_day'] = comma_format($context['member']['real_posts'] / $days_registered, 3);
291
		}
292
	}
293
294
	/**
295
	 * Show age and birthday data if applicable.
296
	 */
297
	private function _determine_age_birth()
298
	{
299
		global $context, $txt;
300
301
		// Set the age...
302
		if (empty($context['member']['birth_date']))
303
		{
304
			$context['member']['age'] = $txt['not_applicable'];
305
			$context['member']['today_is_birthday'] = false;
306
		}
307
		else
308
		{
309
			[$birth_year, $birth_month, $birth_day] = sscanf($context['member']['birth_date'], '%d-%d-%d');
310
			$datearray = getdate(forum_time());
311
			$context['member']['age'] = $birth_year <= 4 ? $txt['not_applicable'] : $datearray['year'] - $birth_year - (($datearray['mon'] > $birth_month || ($datearray['mon'] === $birth_month && $datearray['mday'] >= $birth_day)) ? 0 : 1);
312
			$context['member']['today_is_birthday'] = $datearray['mon'] === $birth_month && $datearray['mday'] === $birth_day;
313
314
		}
315
	}
316
317
	/**
318
	 * Show IP and hostname information for the users current IP of record.
319
	 */
320
	private function _determine_member_ip()
321
	{
322
		global $context, $modSettings;
323
324
		if (allowedTo('moderate_forum'))
325
		{
326
			// Make sure it's a valid ip address; otherwise, don't bother...
327
			if (empty($modSettings['disableHostnameLookup']) && filter_var($this->_profile['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
328
			{
329
				$context['member']['hostname'] = host_from_ip($this->_profile['ip']);
330
			}
331
			else
332
			{
333
				$context['member']['hostname'] = '';
334
			}
335
336
			$context['can_see_ip'] = true;
337
		}
338
		else
339
		{
340
			$context['can_see_ip'] = false;
341
		}
342
	}
343
344
	/**
345
	 * Determines what action user is "doing" at the time of the summary view
346
	 */
347
	private function _determine_member_action()
348
	{
349
		global $context, $modSettings;
350
351
		if (!empty($modSettings['who_enabled']) && $context['member']['online']['is_online'])
352
		{
353
			include_once(SUBSDIR . '/Who.subs.php');
354
			$action = determineActions($this->_profile['url']);
355
			Txt::load('index');
356
357
			if ($action !== false)
0 ignored issues
show
introduced by
The condition $action !== false is always true.
Loading history...
358
			{
359
				$context['member']['action'] = $action;
360
			}
361
		}
362
	}
363
364
	/**
365
	 * Checks if hte member is activated
366
	 *
367
	 * - Creates a link if the viewing member can activate a user
368
	 */
369
	private function _determine_member_activation()
370
	{
371
		global $context, $txt;
372
373
		// If the user is awaiting activation, and the viewer has permission - setup some activation context messages.
374
		if ($context['member']['is_activated'] % 10 !== 1 && allowedTo('moderate_forum'))
375
		{
376
			$context['activate_type'] = $context['member']['is_activated'];
377
378
			// What should the link text be?
379
			$context['activate_link_text'] = in_array((int) $context['member']['is_activated'], [3, 4, 5, 13, 14, 15])
380
				? $txt['account_approve']
381
				: $txt['account_activate'];
382
383
			// Should we show a custom message?
384
			$context['activate_message'] = $txt['account_activate_method_' . $context['member']['is_activated'] % 10] ?? $txt['account_not_activated'];
385
386
			$context['activate_url'] = getUrl('action', ['action' => 'profile', 'save', 'area' => 'activateaccount', 'u' => $this->_memID, '{session_data}', $context['profile-aa' . $this->_memID . '_token_var'] => $context['profile-aa' . $this->_memID . '_token']]);
387
		}
388
	}
389
390
	/**
391
	 * Checks if a member has been banned
392
	 */
393
	private function _determine_member_bans()
394
	{
395
		global $context;
396
397
		// How about, are they banned?
398
		if (allowedTo('moderate_forum'))
399
		{
400
			require_once(SUBSDIR . '/Bans.subs.php');
401
402
			$hostname = empty($context['member']['hostname']) ? '' : $context['member']['hostname'];
403
			$email = empty($context['member']['email']) ? '' : $context['member']['email'];
404
			$context['member']['bans'] = BanCheckUser($this->_memID, $hostname, $email);
405
406
			// Can they edit the ban?
407
			$context['can_edit_ban'] = allowedTo('manage_bans');
408
		}
409
	}
410
411
	/**
412
	 * Show all posts by the current user.
413
	 *
414
	 * @todo This function needs to be split up properly.
415
	 */
416
	public function action_showPosts()
417
	{
418
		global $txt, $modSettings, $context, $board;
419
420
		// Some initial context.
421
		$context['start'] = $this->_req->getQuery('start', 'intval', 0);
422
		$context['current_member'] = $this->_memID;
423
424
		// What are we viewing
425
		$action = $this->_req->getQuery('sa', 'trim', '');
426
		$action_title = ['messages' => 'Messages', 'attach' => 'Attachments', 'topics' => 'Topics', 'unwatchedtopics' => 'Unwatched'];
427
		$action_title = $action_title[$action] ?? 'Posts';
428
429
		theme()->getTemplates()->load('ProfileInfo');
430
431
		// Create the tabs for the template.
432
		$context[$context['profile_menu_name']]['object']->prepareTabData([
433
			'title' => $txt['show' . $action_title],
434
			'description' => $txt['show' . $action_title . '_help'] ?? sprintf($txt['showGeneric_help'], $txt['show' . $action_title]),
435
			'class' => 'i-post-text',
436
		]);
437
438
		// Set the page title
439
		$context['page_title'] = $txt['showPosts'] . ' - ' . $this->_profile['real_name'];
440
441
		// Is the load average too high to allow searching just now?
442
		if ($this->isOverLoadAverage())
443
		{
444
			throw new Exception('loadavg_show_posts_disabled', false);
445
		}
446
447
		// If we're specifically dealing with attachments use that function!
448
		if ($action === 'attach')
449
		{
450
			$this->action_showAttachments();
451
			return;
452
		}
453
454
		// Instead, if we're dealing with unwatched topics (and the feature is enabled) use that other function.
455
		if ($action === 'unwatchedtopics' && $modSettings['enable_unwatch'])
456
		{
457
			$this->action_showUnwatched();
458
			return;
459
		}
460
461
		// Are we just viewing topics?
462
		$context['is_topics'] = $action === 'topics';
463
464
		// If just deleting a message, do it and then redirect back.
465
		if (isset($this->_req->query->delete) && !$context['is_topics'])
466
		{
467
			checkSession('get');
468
469
			// We can be lazy, since removeMessage() will check the permissions for us.
470
			$remover = new MessagesDelete($modSettings['recycle_enable'], $modSettings['recycle_board']);
471
			$remover->removeMessage((int) $this->_req->query->delete);
472
473
			// Back to... where we are now ;).
474
			redirectexit('action=profile;u=' . $this->_memID . ';area=showposts;start=' . $context['start']);
475
		}
476
477
		$msgCount = $context['is_topics'] ? count_user_topics($this->_memID, $board) : count_user_posts($this->_memID, $board);
478
479
		[$min_msg_member, $max_msg_member] = findMinMaxUserMessage($this->_memID, $board);
480
		$range_limit = '';
481
		$maxIndex = (int) $modSettings['defaultMaxMessages'];
482
483
		// Make sure the starting place makes sense and construct our friend the page index.
484
		$context['page_index'] = constructPageIndex('{scripturl}?action=profile;u=' . $this->_memID . ';area=showposts' . ($context['is_topics'] ? ';sa=topics' : ';sa=messages') . (empty($board) ? '' : ';board=' . $board), $context['start'], $msgCount, $maxIndex);
485
		$context['current_page'] = $context['start'] / $maxIndex;
486
487
		// Reverse the query if we're past 50% of the pages for better performance.
488
		$start = $context['start'];
489
		$reverse = $this->_req->getQuery('start', 'intval', 0) > $msgCount / 2;
490
		if ($reverse)
491
		{
492
			$maxIndex = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : (int) $modSettings['defaultMaxMessages'];
493
			$start = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 || $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] ? 0 : $msgCount - $context['start'] - $modSettings['defaultMaxMessages'];
494
		}
495
496
		// Guess the range of messages to be shown to help minimize what the query needs to do
497
		if ($msgCount > 1000)
498
		{
499
			$margin = floor(($max_msg_member - $min_msg_member) * (($start + $modSettings['defaultMaxMessages']) / $msgCount) + .1 * ($max_msg_member - $min_msg_member));
500
501
			// Make a bigger margin for topics only.
502
			if ($context['is_topics'])
503
			{
504
				$margin *= 5;
505
				$range_limit = $reverse ? 't.id_first_msg < ' . ($min_msg_member + $margin) : 't.id_first_msg > ' . ($max_msg_member - $margin);
506
			}
507
			else
508
			{
509
				$range_limit = $reverse ? 'm.id_msg < ' . ($min_msg_member + $margin) : 'm.id_msg > ' . ($max_msg_member - $margin);
510
			}
511
		}
512
513
		// Find this user's posts or topics started
514
		if ($context['is_topics'])
515
		{
516
			$rows = load_user_topics($this->_memID, $start, $maxIndex, $range_limit, $reverse, $board);
517
		}
518
		else
519
		{
520
			$rows = load_user_posts($this->_memID, $start, $maxIndex, $range_limit, $reverse, $board);
521
		}
522
523
		// Start counting at the number of the first message displayed.
524
		$counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start'];
525
		$context['posts'] = [];
526
		$board_ids = ['own' => [], 'any' => []];
527
		$bbc_parser = ParserWrapper::instance();
528
		foreach ($rows as $row)
529
		{
530
			// Censor....
531
			$row['body'] = censor($row['body']);
532
			$row['subject'] = censor($row['subject']);
533
534
			// Do the code.
535
			$row['body'] = $bbc_parser->parseMessage($row['body'], $row['smileys_enabled']);
536
537
			// And the array...
538
			$context['posts'][$counter += $reverse ? -1 : 1] = [
539
				'body' => $row['body'],
540
				'counter' => $counter,
541
				'category' => [
542
					'name' => $row['cname'],
543
					'id' => $row['id_cat']
544
				],
545
				'board' => [
546
					'name' => $row['bname'],
547
					'id' => $row['id_board'],
548
					'link' => '<a href="' . getUrl('board', ['board' => $row['id_board'], 'start' => 0, 'name' => $row['bname']]) . '">' . $row['bname'] . '</a>',
549
				],
550
				'topic' => [
551
					'id' => $row['id_topic'],
552
					'link' => '<a href="' . getUrl('topic', ['topic' => $row['id_topic'], 'msg' => $row['id_msg'], 'subject' => $row['subject'], 'start' => '0']) . '#msg' . $row['id_msg'] . '">' . $row['subject'] . '</a>',
553
				],
554
				'subject' => $row['subject'],
555
				'start' => 'msg' . $row['id_msg'],
556
				'time' => standardTime($row['poster_time']),
557
				'html_time' => htmlTime($row['poster_time']),
558
				'timestamp' => forum_time(true, $row['poster_time']),
559
				'id' => $row['id_msg'],
560
				'tests' => [
561
					'can_reply' => false,
562
					'can_mark_notify' => false,
563
					'can_delete' => false,
564
				],
565
				'delete_possible' => ($row['id_first_msg'] != $row['id_msg'] || $row['id_last_msg'] == $row['id_msg']) && (empty($modSettings['edit_disable_time']) || $row['poster_time'] + $modSettings['edit_disable_time'] * 60 >= time()),
566
				'approved' => $row['approved'],
567
568
				'buttons' => [
569
					// How about... even... remove it entirely?!
570
					'remove' => [
571
						'href' => getUrl('action', ['action' => 'deletemsg', 'msg' => $row['id_msg'], 'topic' => $row['id_topic'], 'profile', 'u' => $context['member']['id'], 'start' => $context['start'], '{session_data}']),
572
						'text' => $txt['remove'],
573
						'test' => 'can_delete',
574
						'custom' => 'onclick="return confirm(' . JavaScriptEscape($txt['remove_message'] . '?') . ');"',
575
					],
576
					// Can we request notification of topics?
577
					'notify' => [
578
						'href' => getUrl('action', ['action' => 'notify', 'topic' => $row['id_topic'], 'msg' => $row['id_msg']]),
579
						'text' => $txt['notify'],
580
						'test' => 'can_mark_notify',
581
					],
582
					// If they *can* reply?
583
					'reply' => [
584
						'href' => getUrl('action', ['action' => 'post', 'topic' => $row['id_topic'], 'msg' => $row['id_msg']]),
585
						'text' => $txt['reply'],
586
						'test' => 'can_reply',
587
					],
588
					// If they *can* quote?
589
					'quote' => [
590
						'href' => getUrl('action', ['action' => 'post', 'topic' => $row['id_topic'], 'msg' => $row['id_msg'], 'quote' => $row['id_msg']]),
591
						'text' => $txt['quote'],
592
						'test' => 'can_quote',
593
					],
594
				]
595
			];
596
597
			if ($this->user->id == $row['id_member_started'])
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...
598
			{
599
				$board_ids['own'][$row['id_board']][] = $counter;
600
			}
601
602
			$board_ids['any'][$row['id_board']][] = $counter;
603
		}
604
605
		// All posts were retrieved in reverse order, get them right again.
606
		if ($reverse)
607
		{
608
			$context['posts'] = array_reverse($context['posts'], true);
609
		}
610
611
		// These are all the permissions that are different from board to board..
612
		if ($context['is_topics'])
613
		{
614
			$permissions = [
615
				'own' => [
616
					'post_reply_own' => 'can_reply',
617
				],
618
				'any' => [
619
					'post_reply_any' => 'can_reply',
620
					'mark_any_notify' => 'can_mark_notify',
621
				]
622
			];
623
		}
624
		else
625
		{
626
			$permissions = [
627
				'own' => [
628
					'post_reply_own' => 'can_reply',
629
					'delete_own' => 'can_delete',
630
				],
631
				'any' => [
632
					'post_reply_any' => 'can_reply',
633
					'mark_any_notify' => 'can_mark_notify',
634
					'delete_any' => 'can_delete',
635
				]
636
			];
637
		}
638
639
		// For every permission in the own/any lists...
640
		foreach ($permissions as $type => $list)
641
		{
642
			foreach ($list as $permission => $allowed)
643
			{
644
				// Get the boards they can do this on...
645
				$boards = boardsAllowedTo($permission);
646
647
				// Hmm, they can do it on all boards, can they?
648
				if (!empty($boards) && $boards[0] == 0)
649
				{
650
					$boards = array_keys($board_ids[$type]);
651
				}
652
653
				// Now go through each board they can do the permission on.
654
				foreach ($boards as $board_id)
655
				{
656
					// There aren't any posts displayed from this board.
657
					if (!isset($board_ids[$type][$board_id]))
658
					{
659
						continue;
660
					}
661
662
					// Set the permission to true ;).
663
					foreach ($board_ids[$type][$board_id] as $counter)
664
					{
665
						$context['posts'][$counter]['tests'][$allowed] = true;
666
					}
667
				}
668
			}
669
		}
670
671
		// Clean up after posts that cannot be deleted and quoted.
672
		$quote_enabled = empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']), true);
673
		foreach ($context['posts'] as $counter => $dummy)
674
		{
675
			$context['posts'][$counter]['tests']['can_delete'] = $context['posts'][$counter]['tests']['can_delete'] && $context['posts'][$counter]['delete_possible'];
676
			$context['posts'][$counter]['tests']['can_quote'] = $context['posts'][$counter]['tests']['can_reply'] && $quote_enabled;
677
		}
678
	}
679
680
	/**
681
	 * Show all the attachments of a user.
682
	 */
683
	public function action_showAttachments()
684
	{
685
		global $txt, $modSettings, $context;
686
687
		// OBEY permissions!
688
		$boardsAllowed = boardsAllowedTo('view_attachments');
689
690
		// Make sure we can't actually see anything...
691
		if (empty($boardsAllowed))
692
		{
693
			$boardsAllowed = [-1];
694
		}
695
696
		// This is all the information required to list attachments.
697
		$listOptions = [
698
			'id' => 'profile_attachments',
699
			'title' => $txt['showAttachments'] . ($context['user']['is_owner'] ? '' : ' - ' . $context['member']['name']),
700
			'items_per_page' => $modSettings['defaultMaxMessages'],
701
			'no_items_label' => $txt['show_attachments_none'],
702
			'base_href' => getUrl('action', ['action' => 'profile', 'area' => 'showposts', 'sa' => 'attach', 'u' => $this->_memID]),
703
			'default_sort_col' => 'filename',
704
			'get_items' => [
705
				'function' => fn($start, $items_per_page, $sort, $boardsAllowed) => $this->list_getAttachments($start, $items_per_page, $sort, $boardsAllowed),
706
				'params' => [
707
					$boardsAllowed,
708
				],
709
			],
710
			'get_count' => [
711
				'function' => fn($boardsAllowed) => $this->list_getNumAttachments($boardsAllowed),
712
				'params' => [
713
					$boardsAllowed,
714
				],
715
			],
716
			'data_check' => [
717
				'class' => static fn($data) => $data['approved'] ? '' : 'approvebg',
718
			],
719
			'columns' => [
720
				'filename' => [
721
					'header' => [
722
						'value' => $txt['show_attach_filename'],
723
						'class' => 'lefttext grid25',
724
					],
725
					'data' => [
726
						'db' => 'filename',
727
					],
728
					'sort' => [
729
						'default' => 'a.filename',
730
						'reverse' => 'a.filename DESC',
731
					],
732
				],
733
				'thumb' => [
734
					'header' => [
735
						'value' => '',
736
					],
737
					'data' => [
738
						'function' => static function ($rowData) {
739
							if ($rowData['is_image'] && !empty($rowData['id_thumb']))
740
							{
741
								return '<img src="' . getUrl('action', ['action' => 'dlattach', 'attach' => $rowData['id_thumb'], 'image']) . '" loading="lazy" />';
742
							}
743
744
							return '<img src="' . getUrl('action', ['action' => 'dlattach', 'attach' => $rowData['id'], 'thumb']) . '" loading="lazy" />';
745
						},
746
						'class' => 'centertext recent_attachments',
747
					],
748
					'sort' => [
749
						'default' => 'a.filename',
750
						'reverse' => 'a.filename DESC',
751
					],
752
				],
753
				'downloads' => [
754
					'header' => [
755
						'value' => $txt['show_attach_downloads'],
756
						'class' => 'centertext',
757
					],
758
					'data' => [
759
						'db' => 'downloads',
760
						'comma_format' => true,
761
						'class' => 'centertext',
762
					],
763
					'sort' => [
764
						'default' => 'a.downloads',
765
						'reverse' => 'a.downloads DESC',
766
					],
767
				],
768
				'subject' => [
769
					'header' => [
770
						'value' => $txt['message'],
771
						'class' => 'lefttext grid30',
772
					],
773
					'data' => [
774
						'db' => 'subject',
775
					],
776
					'sort' => [
777
						'default' => 'm.subject',
778
						'reverse' => 'm.subject DESC',
779
					],
780
				],
781
				'posted' => [
782
					'header' => [
783
						'value' => $txt['show_attach_posted'],
784
						'class' => 'lefttext',
785
					],
786
					'data' => [
787
						'db' => 'posted',
788
						'timeformat' => true,
789
					],
790
					'sort' => [
791
						'default' => 'm.poster_time',
792
						'reverse' => 'm.poster_time DESC',
793
					],
794
				],
795
			],
796
		];
797
798
		// Create the request list.
799
		createList($listOptions);
800
801
		$context['sub_template'] = 'show_list';
802
		$context['default_list'] = 'profile_attachments';
803
	}
804
805
	/**
806
	 * Get a list of attachments for this user
807
	 * Callback for createList()
808
	 *
809
	 * @param int $start The item to start with (for pagination purposes)
810
	 * @param int $items_per_page The number of items to show per page
811
	 * @param string $sort A string indicating how to sort the results
812
	 * @param int[] $boardsAllowed
813
	 *
814
	 * @return array
815
	 */
816
	public function list_getAttachments($start, $items_per_page, $sort, $boardsAllowed)
817
	{
818
		// @todo tweak this method to use $context, etc,
819
		// then call subs function with params set.
820
		return profileLoadAttachments($start, $items_per_page, $sort, $boardsAllowed, $this->_memID);
821
	}
822
823
	/**
824
	 * Callback for createList()
825
	 *
826
	 * @param int[] $boardsAllowed
827
	 *
828
	 * @return int
829
	 */
830
	public function list_getNumAttachments($boardsAllowed)
831
	{
832
		// @todo tweak this method to use $context, etc,
833
		// then call subs function with params set.
834
		return getNumAttachments($boardsAllowed, $this->_memID);
835
	}
836
837
	/**
838
	 * Show all the unwatched topics.
839
	 */
840
	public function action_showUnwatched()
841
	{
842
		global $txt, $modSettings, $context;
843
844
		// Only the owner can see the list (if the function is enabled of course)
845
		if ($this->user->id != $this->_memID || !$modSettings['enable_unwatch'])
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...
846
		{
847
			return;
848
		}
849
850
		// And here they are: the topics you don't like
851
		$listOptions = [
852
			'id' => 'unwatched_topics',
853
			'title' => $txt['showUnwatched'],
854
			'items_per_page' => $modSettings['defaultMaxMessages'],
855
			'no_items_label' => $txt['unwatched_topics_none'],
856
			'base_href' => getUrl('action', ['action' => 'profile', 'area' => 'showposts', 'sa' => 'unwatchedtopics', 'u' => $this->_memID]),
857
			'default_sort_col' => 'started_on',
858
			'get_items' => [
859
				'function' => fn($start, $items_per_page, $sort) => $this->list_getUnwatched($start, $items_per_page, $sort),
860
			],
861
			'get_count' => [
862
				'function' => fn() => $this->list_getNumUnwatched(),
863
			],
864
			'columns' => [
865
				'subject' => [
866
					'header' => [
867
						'value' => $txt['subject'],
868
						'class' => 'lefttext',
869
						'style' => 'width: 30%;',
870
					],
871
					'data' => [
872
						'sprintf' => [
873
							'format' => '<a href="' . getUrl('profile', ['topic' => '%1$d.0']) . '">%2$s</a>',
874
							'params' => [
875
								'id_topic' => false,
876
								'subject' => false,
877
							],
878
						],
879
					],
880
					'sort' => [
881
						'default' => 'm.subject',
882
						'reverse' => 'm.subject DESC',
883
					],
884
				],
885
				'started_by' => [
886
					'header' => [
887
						'value' => $txt['started_by'],
888
						'style' => 'width: 15%;',
889
					],
890
					'data' => [
891
						'db' => 'started_by',
892
					],
893
					'sort' => [
894
						'default' => 'mem.real_name',
895
						'reverse' => 'mem.real_name DESC',
896
					],
897
				],
898
				'started_on' => [
899
					'header' => [
900
						'value' => $txt['on'],
901
						'class' => 'lefttext',
902
						'style' => 'width: 20%;',
903
					],
904
					'data' => [
905
						'db' => 'started_on',
906
						'timeformat' => true,
907
					],
908
					'sort' => [
909
						'default' => 'm.poster_time',
910
						'reverse' => 'm.poster_time DESC',
911
					],
912
				],
913
				'last_post_by' => [
914
					'header' => [
915
						'value' => $txt['last_post'],
916
						'style' => 'width: 15%;',
917
					],
918
					'data' => [
919
						'db' => 'last_post_by',
920
					],
921
					'sort' => [
922
						'default' => 'mem.real_name',
923
						'reverse' => 'mem.real_name DESC',
924
					],
925
				],
926
				'last_post_on' => [
927
					'header' => [
928
						'value' => $txt['on'],
929
						'class' => 'lefttext',
930
						'style' => 'width: 20%;',
931
					],
932
					'data' => [
933
						'db' => 'last_post_on',
934
						'timeformat' => true,
935
					],
936
					'sort' => [
937
						'default' => 'm.poster_time',
938
						'reverse' => 'm.poster_time DESC',
939
					],
940
				],
941
			],
942
		];
943
944
		// Create the request list.
945
		createList($listOptions);
946
947
		$context['sub_template'] = 'show_list';
948
		$context['default_list'] = 'unwatched_topics';
949
	}
950
951
	/**
952
	 * Get the relevant topics in the unwatched list
953
	 * Callback for createList()
954
	 *
955
	 * @param int $start The item to start with (for pagination purposes)
956
	 * @param int $items_per_page The number of items to show per page
957
	 * @param string $sort A string indicating how to sort the results
958
	 *
959
	 * @return array
960
	 */
961
	public function list_getUnwatched($start, $items_per_page, $sort)
962
	{
963
		return getUnwatchedBy($start, $items_per_page, $sort, $this->_memID);
964
	}
965
966
	/**
967
	 * Count the number of topics in the unwatched list
968
	 * Callback for createList()
969
	 */
970
	public function list_getNumUnwatched()
971
	{
972
		return getNumUnwatchedBy($this->_memID);
973
	}
974
975
	/**
976
	 * Gets the user stats for display.
977
	 */
978
	public function action_statPanel()
979
	{
980
		global $txt, $context, $modSettings;
981
982
		require_once(SUBSDIR . '/Stats.subs.php');
983
		loadJavascriptFile(['ext/chart.min.js', 'elk_chart.js']);
984
985
		$context['page_title'] = $txt['statPanel_showStats'] . ' ' . $this->_profile['real_name'];
986
987
		// Is the load average too high to allow searching just now?
988
		if (!empty($modSettings['loadavg_userstats']) && $modSettings['current_load'] >= $modSettings['loadavg_userstats'])
989
		{
990
			throw new Exception('loadavg_userstats_disabled', false);
991
		}
992
993
		theme()->getTemplates()->load('ProfileInfo');
994
995
		// General user statistics.
996
		$timeDays = floor($this->_profile['total_time_logged_in'] / 86400);
997
		$timeHours = floor(($this->_profile['total_time_logged_in'] % 86400) / 3600);
998
		$context['time_logged_in'] = ($timeDays > 0 ? $timeDays . $txt['totalTimeLogged2'] : '') . ($timeHours > 0 ? $timeHours . $txt['totalTimeLogged3'] : '') . floor(($this->_profile['total_time_logged_in'] % 3600) / 60) . $txt['totalTimeLogged4'];
999
		$context['num_posts'] = comma_format($this->_profile['posts']);
1000
		$context['likes_given'] = comma_format($this->_profile['likes_given']);
1001
		$context['likes_received'] = comma_format($this->_profile['likes_received']);
1002
1003
		// Menu tab
1004
		$context[$context['profile_menu_name']]['object']->prepareTabData([
1005
			'title' => $txt['statPanel_generalStats'] . ' - ' . $context['member']['name'],
1006
			'class' => 'i-poll'
1007
		]);
1008
1009
		// Number of topics started.
1010
		$context['num_topics'] = UserStatsTopicsStarted($this->_memID);
1011
1012
		// Number of polls started.
1013
		$context['num_polls'] = UserStatsPollsStarted($this->_memID);
1014
1015
		// Number of polls voted in.
1016
		$context['num_votes'] = UserStatsPollsVoted($this->_memID);
1017
1018
		// Format the numbers...
1019
		$context['num_topics'] = comma_format($context['num_topics']);
1020
		$context['num_polls'] = comma_format($context['num_polls']);
1021
		$context['num_votes'] = comma_format($context['num_votes']);
1022
1023
		// Grab the boards this member posted in most often.
1024
		$context['popular_boards'] = UserStatsMostPostedBoard($this->_memID);
1025
1026
		// Now get the 10 boards this user has most often participated in.
1027
		$context['board_activity'] = UserStatsMostActiveBoard($this->_memID);
1028
1029
		// Posting activity by time.
1030
		$context['posts_by_time'] = UserStatsPostingTime($this->_memID);
1031
1032
		// Custom stats (just add a template_layer to add it to the template!)
1033
		call_integration_hook('integrate_profile_stats', [$this->_memID]);
1034
	}
1035
1036
	/**
1037
	 * Show permissions for a user.
1038
	 */
1039
	public function action_showPermissions()
1040
	{
1041
		global $txt, $board, $context;
1042
1043
		// Verify if the user has sufficient permissions.
1044
		isAllowedTo('manage_permissions');
1045
1046
		Txt::load('ManagePermissions');
1047
		Txt::load('Admin');
1048
		theme()->getTemplates()->load('ManageMembers');
1049
		theme()->getTemplates()->load('ProfileInfo');
1050
1051
		// Load all the permission profiles.
1052
		require_once(SUBSDIR . '/ManagePermissions.subs.php');
1053
		loadPermissionProfiles();
1054
1055
		$context['member']['id'] = $this->_memID;
1056
		$context['member']['name'] = $this->_profile['real_name'];
1057
1058
		$context['page_title'] = $txt['showPermissions'];
1059
		$board = empty($board) ? 0 : (int) $board;
1060
		$context['board'] = $board;
1061
1062
		$curGroups = empty($this->_profile['additional_groups']) ? [] : explode(',', $this->_profile['additional_groups']);
0 ignored issues
show
Bug introduced by
It seems like $this->_profile['additional_groups'] can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1062
		$curGroups = empty($this->_profile['additional_groups']) ? [] : explode(',', /** @scrutinizer ignore-type */ $this->_profile['additional_groups']);
Loading history...
1063
		$curGroups[] = $this->_profile['id_group'];
1064
		$curGroups[] = $this->_profile['id_post_group'];
1065
		$curGroups = array_map('intval', $curGroups);
1066
1067
		// Load a list of boards for the jump box - except the defaults.
1068
		require_once(SUBSDIR . '/Boards.subs.php');
1069
		$board_list = getBoardList(['moderator' => $this->_memID], true);
1070
1071
		$context['boards'] = [];
1072
		$context['no_access_boards'] = [];
1073
		foreach ($board_list as $row)
1074
		{
1075
			$row['id_board'] = (int) $row['id_board'];
1076
			$row['id_profile'] = (int) $row['id_profile'];
1077
			if (!$row['is_mod'] && array_intersect($curGroups, explode(',', $row['member_groups'])) === [])
1078
			{
1079
				$context['no_access_boards'][] = [
1080
					'id' => $row['id_board'],
1081
					'name' => $row['board_name'],
1082
					'is_last' => false,
1083
				];
1084
			}
1085
			elseif ($row['id_profile'] !== 1 || $row['is_mod'])
1086
			{
1087
				$context['boards'][$row['id_board']] = [
1088
					'id' => $row['id_board'],
1089
					'name' => $row['board_name'],
1090
					'url' => getUrl('board', ['board' => $row['id_board'], 'start' => 0, 'name' => $row['board_name']]),
1091
					'selected' => $board === $row['id_board'],
1092
					'profile' => $row['id_profile'],
1093
					'profile_name' => $context['profiles'][$row['id_profile']]['name'],
1094
				];
1095
			}
1096
		}
1097
1098
		if (!empty($context['no_access_boards']))
1099
		{
1100
			$context['no_access_boards'][count($context['no_access_boards']) - 1]['is_last'] = true;
1101
		}
1102
1103
		$context['member']['permissions'] = [
1104
			'general' => [],
1105
			'board' => []
1106
		];
1107
1108
		// If you're an admin we know you can do everything, we might as well leave.
1109
		$context['member']['has_all_permissions'] = in_array(1, $curGroups, true);
1110
		if ($context['member']['has_all_permissions'])
1111
		{
1112
			return;
1113
		}
1114
1115
		// Get all general and board permissions for the groups this member is in
1116
		$context['member']['permissions'] = [
1117
			'general' => getMemberGeneralPermissions($curGroups),
1118
			'board' => getMemberBoardPermissions($this->_memID, $curGroups, $board)
1119
		];
1120
	}
1121
1122
	/**
1123
	 * View a members warnings.
1124
	 */
1125
	public function action_viewWarning()
1126
	{
1127
		global $modSettings, $context, $txt;
1128
1129
		// Firstly, can we actually even be here?
1130
		if ((empty($modSettings['warning_show']) || ((int) $modSettings['warning_show'] === 1 && !$context['user']['is_owner'])) && !allowedTo('issue_warning'))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (empty($modSettings['war...owedTo('issue_warning'), Probably Intended Meaning: empty($modSettings['warn...wedTo('issue_warning'))
Loading history...
1131
		{
1132
			throw new Exception('no_access', false);
1133
		}
1134
1135
		theme()->getTemplates()->load('ProfileInfo');
1136
1137
		// We need this because of template_load_warning_variables
1138
		theme()->getTemplates()->load('Profile');
1139
1140
		// Make sure things which are disabled stay disabled.
1141
		$modSettings['warning_watch'] = empty($modSettings['warning_watch']) ? 110 : $modSettings['warning_watch'];
1142
		$modSettings['warning_moderate'] = !empty($modSettings['warning_moderate']) && !empty($modSettings['postmod_active']) ? $modSettings['warning_moderate'] : 110;
1143
		$modSettings['warning_mute'] = empty($modSettings['warning_mute']) ? 110 : $modSettings['warning_mute'];
1144
1145
		// Let's use a generic list to get all the current warnings
1146
		// and use the issue warnings grab-a-granny thing.
1147
		$listOptions = [
1148
			'id' => 'view_warnings',
1149
			'title' => $txt['profile_viewwarning_previous_warnings'],
1150
			'items_per_page' => $modSettings['defaultMaxMessages'],
1151
			'no_items_label' => $txt['profile_viewwarning_no_warnings'],
1152
			'base_href' => getUrl('action', ['action' => 'profile', 'area' => 'viewwarning', 'sa' => 'user', 'u' => $this->_memID]),
1153
			'default_sort_col' => 'log_time',
1154
			'get_items' => [
1155
				'function' => 'list_getUserWarnings',
1156
				'params' => [
1157
					$this->_memID,
1158
				],
1159
			],
1160
			'get_count' => [
1161
				'function' => 'list_getUserWarningCount',
1162
				'params' => [
1163
					$this->_memID,
1164
				],
1165
			],
1166
			'columns' => [
1167
				'log_time' => [
1168
					'header' => [
1169
						'value' => $txt['profile_warning_previous_time'],
1170
					],
1171
					'data' => [
1172
						'db' => 'time',
1173
					],
1174
					'sort' => [
1175
						'default' => 'lc.log_time DESC',
1176
						'reverse' => 'lc.log_time',
1177
					],
1178
				],
1179
				'reason' => [
1180
					'header' => [
1181
						'value' => $txt['profile_warning_previous_reason'],
1182
						'style' => 'width: 50%;',
1183
					],
1184
					'data' => [
1185
						'db' => 'reason',
1186
					],
1187
				],
1188
				'level' => [
1189
					'header' => [
1190
						'value' => $txt['profile_warning_previous_level'],
1191
					],
1192
					'data' => [
1193
						'db' => 'counter',
1194
					],
1195
					'sort' => [
1196
						'default' => 'lc.counter DESC',
1197
						'reverse' => 'lc.counter',
1198
					],
1199
				],
1200
			],
1201
			'additional_rows' => [
1202
				[
1203
					'position' => 'after_title',
1204
					'value' => $txt['profile_viewwarning_desc'],
1205
					'class' => 'smalltext',
1206
					'style' => 'padding: 2ex;',
1207
				],
1208
			],
1209
		];
1210
1211
		// Create the list for viewing.
1212
		createList($listOptions);
1213
1214
		// Create some common text bits for the template.
1215
		$context['level_effects'] = [
1216
			0 => '',
1217
			$modSettings['warning_watch'] => $txt['profile_warning_effect_own_watched'],
1218
			$modSettings['warning_moderate'] => $txt['profile_warning_effect_own_moderated'],
1219
			$modSettings['warning_mute'] => $txt['profile_warning_effect_own_muted'],
1220
		];
1221
		$context['current_level'] = 0;
1222
		$context['sub_template'] = 'viewWarning';
1223
1224
		foreach ($context['level_effects'] as $limit => $dummy)
1225
		{
1226
			if ($context['member']['warning'] >= $limit)
1227
			{
1228
				$context['current_level'] = $limit;
1229
			}
1230
		}
1231
	}
1232
1233
	/**
1234
	 * Collect and output data related to the profile buddy tab
1235
	 *
1236
	 * - Ajax call from profile info buddy tab
1237
	 */
1238
	public function action_profile_buddies()
1239
	{
1240
		global $context;
1241
1242
		checkSession('get');
1243
1244
		// Need the ProfileInfo and Index (for helper functions) templates
1245
		theme()->getTemplates()->load('ProfileInfo');
1246
1247
		// Prep for a buddy check
1248
		$this->_register_summarytabs();
1249
		$this->_define_user_values();
1250
1251
		// This is returned only for ajax request to a jqueryUI tab
1252
		theme()->getLayers()->removeAll();
1253
1254
		// Some buddies for you
1255
		if (in_array('buddies', $this->_summary_areas, true))
1256
		{
1257
			$this->_load_buddies();
1258
			$context['sub_template'] = 'profile_block_buddies';
1259
		}
1260
	}
1261
1262
	/**
1263
	 * Load the buddies tab with their buddies, real or imaginary
1264
	 */
1265
	private function _load_buddies()
1266
	{
1267
		global $context, $modSettings;
1268
1269
		// Would you be mine? Could you be mine? Be my buddy :D
1270
		$context['buddies'] = [];
1271
		if (empty($modSettings['enable_buddylist']))
1272
		{
1273
			return;
1274
		}
1275
		if (!$context['user']['is_owner'])
1276
		{
1277
			return;
1278
		}
1279
		if (empty($this->user->buddies))
0 ignored issues
show
Bug Best Practice introduced by
The property buddies does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1280
		{
1281
			return;
1282
		}
1283
		if (!in_array('buddies', $this->_summary_areas, true))
1284
		{
1285
			return;
1286
		}
1287
		if (!MembersList::load($this->user->buddies, false, 'profile'))
1288
		{
1289
			return;
1290
		}
1291
1292
		// Get the info for this buddy
1293
		foreach ($this->user->buddies as $buddy)
1294
		{
1295
			$member = MembersList::get($buddy);
1296
			$member->loadContext(true);
1297
1298
			$context['buddies'][$buddy] = $member;
1299
		}
1300
	}
1301
1302
	/**
1303
	 * Collect and output data related to the profile recent tab
1304
	 *
1305
	 * - Ajax call from profile info recent tab
1306
	 */
1307
	public function action_profile_recent()
1308
	{
1309
		global $context;
1310
1311
		checkSession('get');
1312
1313
		// Prep for recent activity
1314
		$this->_register_summarytabs();
1315
		$this->_define_user_values();
1316
1317
		// The block templates are here
1318
		theme()->getTemplates()->load('ProfileInfo');
1319
		$context['sub_template'] = 'profile_blocks';
1320
		$context['profile_blocks'] = [];
1321
1322
		// Flush everything since we intend to return the information to an ajax handler
1323
		theme()->getLayers()->removeAll();
1324
1325
		// So, just what have you been up to?
1326
		if (in_array('posts', $this->_summary_areas, true))
1327
		{
1328
			$this->_load_recent_posts();
1329
			$context['profile_blocks'][] = 'template_profile_block_posts';
1330
		}
1331
1332
		if (in_array('topics', $this->_summary_areas, true))
1333
		{
1334
			$this->_load_recent_topics();
1335
			$context['profile_blocks'][] = 'template_profile_block_topics';
1336
		}
1337
1338
		if (in_array('attachments', $this->_summary_areas, true))
1339
		{
1340
			$this->_load_recent_attachments();
1341
			$context['profile_blocks'][] = 'template_profile_block_attachments';
1342
		}
1343
	}
1344
1345
	/**
1346
	 * Load a members most recent posts
1347
	 */
1348
	private function _load_recent_posts()
1349
	{
1350
		global $context, $modSettings;
1351
1352
		// How about their most recent posts?
1353
		if (in_array('posts', $this->_summary_areas, true))
1354
		{
1355
			// Is the load average too high just now, then let them know
1356
			$context['loadaverage'] = $this->isOverLoadAverage();
1357
			if (!$context['loadaverage'])
1358
			{
1359
				// Set up to get the last 10 posts of this member
1360
				$msgCount = count_user_posts($this->_memID);
1361
				$range_limit = '';
1362
				$maxIndex = 10;
1363
				$start = $this->_req->getQuery('start', 'intval', 0);
1364
1365
				// If they are a frequent poster, we guess the range to help minimize what the query work
1366
				if ($msgCount > 1000)
1367
				{
1368
					[$min_msg_member, $max_msg_member] = findMinMaxUserMessage($this->_memID);
1369
					$margin = floor(($max_msg_member - $min_msg_member) * (($start + $modSettings['defaultMaxMessages']) / $msgCount) + .1 * ($max_msg_member - $min_msg_member));
1370
					$range_limit = 'm.id_msg > ' . ($max_msg_member - $margin);
1371
				}
1372
1373
				// Find this user's most recent posts
1374
				$rows = load_user_posts($this->_memID, 0, $maxIndex, $range_limit);
1375
				$bbc_parser = ParserWrapper::instance();
1376
				$context['posts'] = [];
1377
				foreach ($rows as $row)
1378
				{
1379
					// Censor....
1380
					$row['body'] = censor($row['body']);
1381
					$row['subject'] = censor($row['subject']);
1382
1383
					// Do the code.
1384
					$row['body'] = $bbc_parser->parseMessage($row['body'], $row['smileys_enabled']);
1385
					$preview = strip_tags(strtr($row['body'], ['<br />' => '&#10;']));
1386
					$preview = Util::shorten_text($preview, empty($modSettings['ssi_preview_length']) ? 128 : $modSettings['ssi_preview_length']);
1387
					$short_subject = Util::shorten_text($row['subject'], empty($modSettings['ssi_subject_length']) ? 24 : $modSettings['ssi_subject_length']);
1388
1389
					// And the array...
1390
					$context['posts'][] = [
1391
						'body' => $preview,
1392
						'board' => [
1393
							'name' => $row['bname'],
1394
							'link' => '<a href="' . getUrl('board', ['board' => $row['id_board'], 'start' => 0, 'name' => $row['bname']]) . '">' . $row['bname'] . '</a>'
1395
						],
1396
						'subject' => $row['subject'],
1397
						'short_subject' => $short_subject,
1398
						'time' => standardTime($row['poster_time']),
1399
						'html_time' => htmlTime($row['poster_time']),
1400
						'timestamp' => forum_time(true, $row['poster_time']),
1401
						'link' => '<a href="' . getUrl('topic', ['topic' => $row['id_topic'], 'start' => 0, 'msg' => $row['id_msg'], 'subject' => $row['subject'], 'hash' => '#msg' . $row['id_msg']]) . '" rel="nofollow">' . $short_subject . '</a>',
1402
					];
1403
				}
1404
			}
1405
		}
1406
	}
1407
1408
	/**
1409
	 * Load a users recent topics
1410
	 */
1411
	private function _load_recent_topics()
1412
	{
1413
		global $context, $modSettings;
1414
1415
		// How about the most recent topics that they started?
1416
		if (in_array('topics', $this->_summary_areas, true))
1417
		{
1418
			// Is the load average still too high?
1419
			$context['loadaverage'] = $this->isOverLoadAverage();
1420
			if (!$context['loadaverage'])
1421
			{
1422
				// Set up to get the last 10 topics of this member
1423
				$topicCount = count_user_topics($this->_memID);
1424
				$range_limit = '';
1425
				$maxIndex = 10;
1426
				$start = $this->_req->getQuery('start', 'intval', 0);
1427
1428
				// If they are a frequent topic starter we guess the range to help the query
1429
				if ($topicCount > 1000)
1430
				{
1431
					[$min_topic_member, $max_topic_member] = findMinMaxUserTopic($this->_memID);
1432
					$margin = floor(($max_topic_member - $min_topic_member) * (($start + $modSettings['defaultMaxMessages']) / $topicCount) + .1 * ($max_topic_member - $min_topic_member));
1433
					$margin *= 5;
1434
					$range_limit = 't.id_first_msg > ' . ($max_topic_member - $margin);
1435
				}
1436
1437
				// Find this user's most recent topics
1438
				$rows = load_user_topics($this->_memID, 0, $maxIndex, $range_limit);
1439
				$context['topics'] = [];
1440
				$bbc_parser = ParserWrapper::instance();
1441
1442
				foreach ($rows as $row)
1443
				{
1444
					// Censor....
1445
					$row['body'] = censor($row['body']);
1446
					$row['subject'] = censor($row['subject']);
1447
1448
					// Do the code.
1449
					$row['body'] = $bbc_parser->parseMessage($row['body'], $row['smileys_enabled']);
1450
					$preview = strip_tags(strtr($row['body'], ['<br />' => '&#10;']));
1451
					$preview = Util::shorten_text($preview, empty($modSettings['ssi_preview_length']) ? 128 : $modSettings['ssi_preview_length']);
1452
					$short_subject = Util::shorten_text($row['subject'], empty($modSettings['ssi_subject_length']) ? 24 : $modSettings['ssi_subject_length']);
1453
1454
					// And the array...
1455
					$context['topics'][] = [
1456
						'board' => [
1457
							'name' => $row['bname'],
1458
							'link' => '<a href="' . getUrl('board', ['board' => $row['id_board'], 'start' => 0, 'name' => $row['bname']]) . '">' . $row['bname'] . '</a>'
1459
						],
1460
						'subject' => $row['subject'],
1461
						'short_subject' => $short_subject,
1462
						'body' => $preview,
1463
						'time' => standardTime($row['poster_time']),
1464
						'html_time' => htmlTime($row['poster_time']),
1465
						'timestamp' => forum_time(true, $row['poster_time']),
1466
						'link' => '<a href="' . getUrl('topic', ['topic' => $row['id_topic'], 'start' => 0, 'msg' => $row['id_msg'], 'subject' => $row['subject'], 'hash' => '#msg' . $row['id_msg']]) . '" rel="nofollow">' . $short_subject . '</a>',
1467
					];
1468
				}
1469
			}
1470
		}
1471
	}
1472
1473
	/**
1474
	 * If they have made recent attachments, lets get a list of them to display
1475
	 */
1476
	private function _load_recent_attachments()
1477
	{
1478
		global $context, $modSettings, $settings;
1479
1480
		$context['thumbs'] = [];
1481
1482
		// Load up the most recent attachments for this user for use in profile views etc.
1483
		if (!empty($modSettings['attachmentEnable'])
1484
			&& !empty($settings['attachments_on_summary'])
1485
			&& in_array('attachments', $this->_summary_areas, true))
1486
		{
1487
			$boardsAllowed = boardsAllowedTo('view_attachments');
1488
1489
			if (empty($boardsAllowed))
1490
			{
1491
				$boardsAllowed = [-1];
1492
			}
1493
1494
			$attachments = $this->list_getAttachments(0, $settings['attachments_on_summary'], 'm.poster_time DESC', $boardsAllowed);
1495
1496
			// Some generic images for mime types
1497
			$mime_images_url = $settings['default_images_url'] . '/mime_images/';
1498
			$mime_path = $settings['default_theme_dir'] . '/images/mime_images/';
1499
1500
			// Load them in to $context for use in the template
1501
			foreach ($attachments as $i => $attachment)
1502
			{
1503
				$context['thumbs'][$i] = [
1504
					'url' => getUrl('action', ['action' => 'dlattach', 'topic' => $attachment['topic'] . '.0', 'attach' => $attachment['id']]),
1505
					'img' => '',
1506
					'filename' => $attachment['filename'],
1507
					'downloads' => $attachment['downloads'],
1508
					'subject' => $attachment['subject'],
1509
					'id' => $attachment['id'],
1510
				];
1511
1512
				// Show a thumbnail image as well?
1513
				if ($attachment['is_image'] && !empty($modSettings['attachmentShowImages']) && !empty($modSettings['attachmentThumbnails']))
1514
				{
1515
					if (!empty($attachment['id_thumb']))
1516
					{
1517
						$context['thumbs'][$i]['img'] = '<img id="thumb_' . $attachment['id'] . '" src="' . getUrl('action', ['action' => 'dlattach', 'topic' => $attachment['topic'] . '.0', 'attach' => $attachment['id_thumb'], 'image']) . '" title="" alt="" loading="lazy" />';
1518
					}
1519
					elseif (!empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']))
1520
					{
1521
						// No thumbnail available ... use html instead
1522
						if ($attachment['width'] > $modSettings['attachmentThumbWidth'] || $attachment['height'] > $modSettings['attachmentThumbHeight'])
1523
						{
1524
							$context['thumbs'][$i]['img'] = '<img id="thumb_' . $attachment['id'] . '" src="' . getUrl('action', ['action' => 'dlattach', 'topic' => $attachment['topic'] . '.0', 'attach' => $attachment['id']]) . '" title="" alt="" width="' . $modSettings['attachmentThumbWidth'] . '" height="' . $modSettings['attachmentThumbHeight'] . '" loading="lazy" />';
1525
						}
1526
						else
1527
						{
1528
							$context['thumbs'][$i]['img'] = '<img id="thumb_' . $attachment['id'] . '" src="' . getUrl('action', ['action' => 'dlattach', 'topic' => $attachment['topic'] . '.0', 'attach' => $attachment['id']]) . '" title="" alt="" width="' . $attachment['width'] . '" height="' . $attachment['height'] . '" loading="lazy" />';
1529
						}
1530
					}
1531
				}
1532
				// Not an image so set a mime thumbnail based off the filetype
1533
				elseif ((!empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight'])) && (128 > $modSettings['attachmentThumbWidth'] || 128 > $modSettings['attachmentThumbHeight']))
1534
				{
1535
					$context['thumbs'][$i]['img'] = '<img src="' . $mime_images_url . (FileFunctions::instance()->fileExists($mime_path . $attachment['fileext'] . '.png') ? $attachment['fileext'] : 'default') . '.png" title="" alt="" width="' . $modSettings['attachmentThumbWidth'] . '" height="' . $modSettings['attachmentThumbHeight'] . '" loading="lazy" />';
1536
				}
1537
				else
1538
				{
1539
					$context['thumbs'][$i]['img'] = '<img src="' . $mime_images_url . (FileFunctions::instance()->fileExists($mime_path . $attachment['fileext'] . '.png') ? $attachment['fileext'] : 'default') . '.png" title="" alt="" loading="lazy" />';
1540
				}
1541
			}
1542
		}
1543
	}
1544
1545
	/**
1546
	 * Checks if the current load average exceeds a specified threshold.
1547
	 *
1548
	 * @return bool Returns true if the current load average is higher than the specified threshold, otherwise false.
1549
	 */
1550
	private function isOverLoadAverage()
1551
	{
1552
		global $modSettings;
1553
1554
		// Is the load average too high just now, then let them know
1555
		return !empty($modSettings['loadavg_show_posts']) && $modSettings['current_load'] >= $modSettings['loadavg_show_posts'];
1556
	}
1557
}
1558