ProfileInfo::_define_user_values()   B
last analyzed

Complexity

Conditions 9
Paths 48

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 12
nc 48
nop 0
dl 0
loc 21
rs 8.0555
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 Beta 1
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 set up the class yourself, or failure 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(): void
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 is 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(): void
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 what's 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(): void
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'] = str_starts_with($modSettings['signature_settings'], "1");
236
	}
237
238
	/**
239
	 * Loads the information needed to create the profile summary view
240
	 */
241
	private function _load_summary(): void
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(): void
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(): void
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(): void
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(): void
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(): void
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 the member is activated
366
	 *
367
	 * - Creates a link if the viewing member can activate a user
368
	 */
369
	private function _determine_member_activation(): void
370
	{
371
		global $context, $txt;
372
373
		// If the user is awaiting activation, and the viewer has permission - set up 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(): void
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(): void
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
			$context['start'] = ($context['start'] >= $msgCount) ? $msgCount - $maxIndex : $context['start'];
493
			$maxIndex = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : (int) $modSettings['defaultMaxMessages'];
494
			$start = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 || $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] ? 0 : $msgCount - $context['start'] - $modSettings['defaultMaxMessages'];
495
		}
496
497
		// Guess the range of messages to be shown to help minimize what the query needs to do
498
		if ($msgCount > 1000)
499
		{
500
			$margin = floor(($max_msg_member - $min_msg_member) * (($start + $modSettings['defaultMaxMessages']) / $msgCount) + .1 * ($max_msg_member - $min_msg_member));
501
502
			// Make a bigger margin for topics only.
503
			if ($context['is_topics'])
504
			{
505
				$margin *= 5;
506
				$range_limit = $reverse ? 't.id_first_msg < ' . ($min_msg_member + $margin) : 't.id_first_msg > ' . ($max_msg_member - $margin);
507
			}
508
			else
509
			{
510
				$range_limit = $reverse ? 'm.id_msg < ' . ($min_msg_member + $margin) : 'm.id_msg > ' . ($max_msg_member - $margin);
511
			}
512
		}
513
514
		// Find this user's posts or topics started
515
		if ($context['is_topics'])
516
		{
517
			$rows = load_user_topics($this->_memID, $start, $maxIndex, $range_limit, $reverse, $board);
518
		}
519
		else
520
		{
521
			$rows = load_user_posts($this->_memID, $start, $maxIndex, $range_limit, $reverse, $board);
522
		}
523
524
		// Start counting at the number of the first message displayed.
525
		$counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start'];
526
		$context['posts'] = [];
527
		$board_ids = ['own' => [], 'any' => []];
528
		$bbc_parser = ParserWrapper::instance();
529
		foreach ($rows as $row)
530
		{
531
			// Censor....
532
			$row['body'] = censor($row['body']);
533
			$row['subject'] = censor($row['subject']);
534
535
			// Do the code.
536
			$row['body'] = $bbc_parser->parseMessage($row['body'], $row['smileys_enabled']);
537
538
			// And the array...
539
			$context['posts'][$counter += $reverse ? -1 : 1] = [
540
				'body' => $row['body'],
541
				'counter' => $counter,
542
				'category' => [
543
					'name' => $row['cname'],
544
					'id' => $row['id_cat']
545
				],
546
				'board' => [
547
					'name' => $row['bname'],
548
					'id' => $row['id_board'],
549
					'link' => '<a href="' . getUrl('board', ['board' => $row['id_board'], 'start' => 0, 'name' => $row['bname']]) . '">' . $row['bname'] . '</a>',
550
				],
551
				'topic' => [
552
					'id' => $row['id_topic'],
553
					'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>',
554
				],
555
				'subject' => $row['subject'],
556
				'start' => 'msg' . $row['id_msg'],
557
				'time' => standardTime($row['poster_time']),
558
				'html_time' => htmlTime($row['poster_time']),
559
				'timestamp' => forum_time(true, $row['poster_time']),
560
				'id' => $row['id_msg'],
561
				'tests' => [
562
					'can_reply' => false,
563
					'can_mark_notify' => false,
564
					'can_delete' => false,
565
				],
566
				'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()),
567
				'approved' => $row['approved'],
568
569
				'buttons' => [
570
					// How about... even... remove it entirely?!
571
					'remove' => [
572
						'href' => getUrl('action', ['action' => 'deletemsg', 'msg' => $row['id_msg'], 'topic' => $row['id_topic'], 'profile', 'u' => $context['member']['id'], 'start' => $context['start'], '{session_data}']),
573
						'text' => $txt['remove'],
574
						'test' => 'can_delete',
575
						'custom' => 'onclick="return confirm(' . JavaScriptEscape($txt['remove_message'] . '?') . ');"',
576
					],
577
					// Can we request notification of topics?
578
					'notify' => [
579
						'href' => getUrl('action', ['action' => 'notify', 'topic' => $row['id_topic'], 'msg' => $row['id_msg']]),
580
						'text' => $txt['notify'],
581
						'test' => 'can_mark_notify',
582
					],
583
					// If they *can* reply?
584
					'reply' => [
585
						'href' => getUrl('action', ['action' => 'post', 'topic' => $row['id_topic'], 'msg' => $row['id_msg']]),
586
						'text' => $txt['reply'],
587
						'test' => 'can_reply',
588
					],
589
					// If they *can* quote?
590
					'quote' => [
591
						'href' => getUrl('action', ['action' => 'post', 'topic' => $row['id_topic'], 'msg' => $row['id_msg'], 'quote' => $row['id_msg']]),
592
						'text' => $txt['quote'],
593
						'test' => 'can_quote',
594
					],
595
				]
596
			];
597
598
			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...
599
			{
600
				$board_ids['own'][$row['id_board']][] = $counter;
601
			}
602
603
			$board_ids['any'][$row['id_board']][] = $counter;
604
		}
605
606
		// All posts were retrieved in reverse order, get them right again.
607
		if ($reverse)
608
		{
609
			$context['posts'] = array_reverse($context['posts'], true);
610
		}
611
612
		// These are all the permissions that are different from board to board.
613
		if ($context['is_topics'])
614
		{
615
			$permissions = [
616
				'own' => [
617
					'post_reply_own' => 'can_reply',
618
				],
619
				'any' => [
620
					'post_reply_any' => 'can_reply',
621
					'mark_any_notify' => 'can_mark_notify',
622
				]
623
			];
624
		}
625
		else
626
		{
627
			$permissions = [
628
				'own' => [
629
					'post_reply_own' => 'can_reply',
630
					'delete_own' => 'can_delete',
631
				],
632
				'any' => [
633
					'post_reply_any' => 'can_reply',
634
					'mark_any_notify' => 'can_mark_notify',
635
					'delete_any' => 'can_delete',
636
				]
637
			];
638
		}
639
640
		// For every permission in the own/any lists...
641
		foreach ($permissions as $type => $list)
642
		{
643
			foreach ($list as $permission => $allowed)
644
			{
645
				// Get the boards they can do this on...
646
				$boards = boardsAllowedTo($permission);
647
648
				// Hmm, they can do it on all boards, can they?
649
				if (!empty($boards) && $boards[0] == 0)
650
				{
651
					$boards = array_keys($board_ids[$type]);
652
				}
653
654
				// Now go through each board they can do the permission on.
655
				foreach ($boards as $board_id)
656
				{
657
					// There aren't any posts displayed from this board.
658
					if (!isset($board_ids[$type][$board_id]))
659
					{
660
						continue;
661
					}
662
663
					// Set the permission to true ;).
664
					foreach ($board_ids[$type][$board_id] as $counter)
665
					{
666
						$context['posts'][$counter]['tests'][$allowed] = true;
667
					}
668
				}
669
			}
670
		}
671
672
		// Clean up after posts that cannot be deleted and quoted.
673
		$quote_enabled = empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']), true);
674
		foreach ($context['posts'] as $counter => $dummy)
675
		{
676
			$context['posts'][$counter]['tests']['can_delete'] = $context['posts'][$counter]['tests']['can_delete'] && $context['posts'][$counter]['delete_possible'];
677
			$context['posts'][$counter]['tests']['can_quote'] = $context['posts'][$counter]['tests']['can_reply'] && $quote_enabled;
678
		}
679
	}
680
681
	/**
682
	 * Show all the attachments of a user.
683
	 */
684
	public function action_showAttachments(): void
685
	{
686
		global $txt, $modSettings, $context;
687
688
		// OBEY permissions!
689
		$boardsAllowed = boardsAllowedTo('view_attachments');
690
691
		// Make sure we can't see anything...
692
		if (empty($boardsAllowed))
693
		{
694
			$boardsAllowed = [-1];
695
		}
696
697
		// This is all the information required to list attachments.
698
		$listOptions = [
699
			'id' => 'profile_attachments',
700
			'title' => $txt['showAttachments'] . ($context['user']['is_owner'] ? '' : ' - ' . $context['member']['name']),
701
			'items_per_page' => $modSettings['defaultMaxMessages'],
702
			'no_items_label' => $txt['show_attachments_none'],
703
			'base_href' => getUrl('action', ['action' => 'profile', 'area' => 'showposts', 'sa' => 'attach', 'u' => $this->_memID]),
704
			'default_sort_col' => 'filename',
705
			'get_items' => [
706
				'function' => fn($start, $items_per_page, $sort, $boardsAllowed) => $this->list_getAttachments($start, $items_per_page, $sort, $boardsAllowed),
707
				'params' => [
708
					$boardsAllowed,
709
				],
710
			],
711
			'get_count' => [
712
				'function' => fn($boardsAllowed) => $this->list_getNumAttachments($boardsAllowed),
713
				'params' => [
714
					$boardsAllowed,
715
				],
716
			],
717
			'data_check' => [
718
				'class' => static fn($data) => $data['approved'] ? '' : 'approvebg',
719
			],
720
			'columns' => [
721
				'filename' => [
722
					'header' => [
723
						'value' => $txt['show_attach_filename'],
724
						'class' => 'lefttext grid25',
725
					],
726
					'data' => [
727
						'db' => 'filename',
728
					],
729
					'sort' => [
730
						'default' => 'a.filename',
731
						'reverse' => 'a.filename DESC',
732
					],
733
				],
734
				'thumb' => [
735
					'header' => [
736
						'value' => '',
737
					],
738
					'data' => [
739
						'function' => static function ($rowData) {
740
							if ($rowData['is_image'] && !empty($rowData['id_thumb']))
741
							{
742
								return '<img src="' . getUrl('action', ['action' => 'dlattach', 'attach' => $rowData['id_thumb'], 'image']) . '" loading="lazy" />';
743
							}
744
745
							return '<img src="' . getUrl('action', ['action' => 'dlattach', 'attach' => $rowData['id'], 'thumb']) . '" loading="lazy" />';
746
						},
747
						'class' => 'centertext recent_attachments',
748
					],
749
					'sort' => [
750
						'default' => 'a.filename',
751
						'reverse' => 'a.filename DESC',
752
					],
753
				],
754
				'downloads' => [
755
					'header' => [
756
						'value' => $txt['show_attach_downloads'],
757
						'class' => 'centertext',
758
					],
759
					'data' => [
760
						'db' => 'downloads',
761
						'comma_format' => true,
762
						'class' => 'centertext',
763
					],
764
					'sort' => [
765
						'default' => 'a.downloads',
766
						'reverse' => 'a.downloads DESC',
767
					],
768
				],
769
				'subject' => [
770
					'header' => [
771
						'value' => $txt['message'],
772
						'class' => 'lefttext grid30',
773
					],
774
					'data' => [
775
						'db' => 'subject',
776
					],
777
					'sort' => [
778
						'default' => 'm.subject',
779
						'reverse' => 'm.subject DESC',
780
					],
781
				],
782
				'posted' => [
783
					'header' => [
784
						'value' => $txt['show_attach_posted'],
785
						'class' => 'lefttext',
786
					],
787
					'data' => [
788
						'db' => 'posted',
789
						'timeformat' => true,
790
					],
791
					'sort' => [
792
						'default' => 'm.poster_time',
793
						'reverse' => 'm.poster_time DESC',
794
					],
795
				],
796
			],
797
		];
798
799
		// Create the request list.
800
		createList($listOptions);
801
802
		$context['sub_template'] = 'show_list';
803
		$context['default_list'] = 'profile_attachments';
804
	}
805
806
	/**
807
	 * Get a list of attachments for this user
808
	 * Callback for createList()
809
	 *
810
	 * @param int $start The item to start with (for pagination purposes)
811
	 * @param int $items_per_page The number of items to show per page
812
	 * @param string $sort A string indicating how to sort the results
813
	 * @param int[] $boardsAllowed
814
	 *
815
	 * @return array
816
	 */
817
	public function list_getAttachments($start, $items_per_page, $sort, $boardsAllowed): array
818
	{
819
		// @todo tweak this method to use $context, etc,
820
		// then call subs function with params set.
821
		return profileLoadAttachments($start, $items_per_page, $sort, $boardsAllowed, $this->_memID);
822
	}
823
824
	/**
825
	 * Callback for createList()
826
	 *
827
	 * @param int[] $boardsAllowed
828
	 *
829
	 * @return int
830
	 */
831
	public function list_getNumAttachments($boardsAllowed): int
832
	{
833
		// @todo tweak this method to use $context, etc,
834
		// then call subs function with params set.
835
		return getNumAttachments($boardsAllowed, $this->_memID);
836
	}
837
838
	/**
839
	 * Show all the unwatched topics.
840
	 */
841
	public function action_showUnwatched(): void
842
	{
843
		global $txt, $modSettings, $context;
844
845
		// Only the owner can see the list (if the function is enabled, of course)
846
		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...
847
		{
848
			return;
849
		}
850
851
		// And here they are: the topics you don't like
852
		$listOptions = [
853
			'id' => 'unwatched_topics',
854
			'title' => $txt['showUnwatched'],
855
			'items_per_page' => $modSettings['defaultMaxMessages'],
856
			'no_items_label' => $txt['unwatched_topics_none'],
857
			'base_href' => getUrl('action', ['action' => 'profile', 'area' => 'showposts', 'sa' => 'unwatchedtopics', 'u' => $this->_memID]),
858
			'default_sort_col' => 'started_on',
859
			'get_items' => [
860
				'function' => fn($start, $items_per_page, $sort) => $this->list_getUnwatched($start, $items_per_page, $sort),
861
			],
862
			'get_count' => [
863
				'function' => fn() => $this->list_getNumUnwatched(),
864
			],
865
			'columns' => [
866
				'subject' => [
867
					'header' => [
868
						'value' => $txt['subject'],
869
						'class' => 'lefttext',
870
						'style' => 'width: 30%;',
871
					],
872
					'data' => [
873
						'sprintf' => [
874
							'format' => '<a href="' . getUrl('profile', ['topic' => '%1$d.0']) . '">%2$s</a>',
875
							'params' => [
876
								'id_topic' => false,
877
								'subject' => false,
878
							],
879
						],
880
					],
881
					'sort' => [
882
						'default' => 'm.subject',
883
						'reverse' => 'm.subject DESC',
884
					],
885
				],
886
				'started_by' => [
887
					'header' => [
888
						'value' => $txt['started_by'],
889
						'style' => 'width: 15%;',
890
					],
891
					'data' => [
892
						'db' => 'started_by',
893
					],
894
					'sort' => [
895
						'default' => 'mem.real_name',
896
						'reverse' => 'mem.real_name DESC',
897
					],
898
				],
899
				'started_on' => [
900
					'header' => [
901
						'value' => $txt['on'],
902
						'class' => 'lefttext',
903
						'style' => 'width: 20%;',
904
					],
905
					'data' => [
906
						'db' => 'started_on',
907
						'timeformat' => true,
908
					],
909
					'sort' => [
910
						'default' => 'm.poster_time',
911
						'reverse' => 'm.poster_time DESC',
912
					],
913
				],
914
				'last_post_by' => [
915
					'header' => [
916
						'value' => $txt['last_post'],
917
						'style' => 'width: 15%;',
918
					],
919
					'data' => [
920
						'db' => 'last_post_by',
921
					],
922
					'sort' => [
923
						'default' => 'mem.real_name',
924
						'reverse' => 'mem.real_name DESC',
925
					],
926
				],
927
				'last_post_on' => [
928
					'header' => [
929
						'value' => $txt['on'],
930
						'class' => 'lefttext',
931
						'style' => 'width: 20%;',
932
					],
933
					'data' => [
934
						'db' => 'last_post_on',
935
						'timeformat' => true,
936
					],
937
					'sort' => [
938
						'default' => 'm.poster_time',
939
						'reverse' => 'm.poster_time DESC',
940
					],
941
				],
942
			],
943
		];
944
945
		// Create the request list.
946
		createList($listOptions);
947
948
		$context['sub_template'] = 'show_list';
949
		$context['default_list'] = 'unwatched_topics';
950
	}
951
952
	/**
953
	 * Get the relevant topics in the unwatched list
954
	 * Callback for createList()
955
	 *
956
	 * @param int $start The item to start with (for pagination purposes)
957
	 * @param int $items_per_page The number of items to show per page
958
	 * @param string $sort A string indicating how to sort the results
959
	 *
960
	 * @return array
961
	 */
962
	public function list_getUnwatched($start, $items_per_page, $sort): array
963
	{
964
		return getUnwatchedBy($start, $items_per_page, $sort, $this->_memID);
965
	}
966
967
	/**
968
	 * Count the number of topics in the unwatched list
969
	 * Callback for createList()
970
	 */
971
	public function list_getNumUnwatched(): int
972
	{
973
		return getNumUnwatchedBy($this->_memID);
974
	}
975
976
	/**
977
	 * Gets the user stats for display.
978
	 */
979
	public function action_statPanel(): void
980
	{
981
		global $txt, $context, $modSettings;
982
983
		require_once(SUBSDIR . '/Stats.subs.php');
984
		loadJavascriptFile(['ext/chart.min.js', 'elk_chart.js']);
985
986
		$context['page_title'] = $txt['statPanel_showStats'] . ' ' . $this->_profile['real_name'];
987
988
		// Is the load average too high to allow searching just now?
989
		if (!empty($modSettings['loadavg_userstats']) && $modSettings['current_load'] >= $modSettings['loadavg_userstats'])
990
		{
991
			throw new Exception('loadavg_userstats_disabled', false);
992
		}
993
994
		theme()->getTemplates()->load('ProfileInfo');
995
996
		// General user statistics.
997
		$timeDays = floor($this->_profile['total_time_logged_in'] / 86400);
998
		$timeHours = floor(($this->_profile['total_time_logged_in'] % 86400) / 3600);
999
		$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'];
1000
		$context['num_posts'] = comma_format($this->_profile['posts']);
1001
		$context['likes_given'] = comma_format($this->_profile['likes_given']);
1002
		$context['likes_received'] = comma_format($this->_profile['likes_received']);
1003
1004
		// Menu tab
1005
		$context[$context['profile_menu_name']]['object']->prepareTabData([
1006
			'title' => $txt['statPanel_generalStats'] . ' - ' . $context['member']['name'],
1007
			'class' => 'i-poll'
1008
		]);
1009
1010
		// The number of topics started.
1011
		$context['num_topics'] = UserStatsTopicsStarted($this->_memID);
1012
1013
		// The number of polls started.
1014
		$context['num_polls'] = UserStatsPollsStarted($this->_memID);
1015
1016
		// The number of polls voted in.
1017
		$context['num_votes'] = UserStatsPollsVoted($this->_memID);
1018
1019
		// Format the numbers...
1020
		$context['num_topics'] = comma_format($context['num_topics']);
1021
		$context['num_polls'] = comma_format($context['num_polls']);
1022
		$context['num_votes'] = comma_format($context['num_votes']);
1023
1024
		// Grab the boards this member posted in most often.
1025
		$context['popular_boards'] = UserStatsMostPostedBoard($this->_memID);
1026
1027
		// Now get the 10 boards this user has most often participated in.
1028
		$context['board_activity'] = UserStatsMostActiveBoard($this->_memID);
1029
1030
		// Posting activity by time.
1031
		$context['posts_by_time'] = UserStatsPostingTime($this->_memID);
1032
1033
		// Custom stats (just add a template_layer to add it to the template!)
1034
		call_integration_hook('integrate_profile_stats', [$this->_memID]);
1035
	}
1036
1037
	/**
1038
	 * Show permissions for a user.
1039
	 */
1040
	public function action_showPermissions(): void
1041
	{
1042
		global $txt, $board, $context;
1043
1044
		// Verify if the user has sufficient permissions.
1045
		isAllowedTo('manage_permissions');
1046
1047
		Txt::load('ManagePermissions');
1048
		Txt::load('Admin');
1049
		theme()->getTemplates()->load('ManageMembers');
1050
		theme()->getTemplates()->load('ProfileInfo');
1051
1052
		// Load all the permission profiles.
1053
		require_once(SUBSDIR . '/ManagePermissions.subs.php');
1054
		loadPermissionProfiles();
1055
1056
		$context['member']['id'] = $this->_memID;
1057
		$context['member']['name'] = $this->_profile['real_name'];
1058
1059
		$context['page_title'] = $txt['showPermissions'];
1060
		$board = empty($board) ? 0 : $board;
1061
		$context['board'] = $board;
1062
1063
		$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

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