Passed
Push — development ( 5482d2...9758e9 )
by Spuds
01:22 queued 35s
created

Profile::_build_profile_breadcrumbs()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 8
nop 0
dl 0
loc 35
rs 9.3888
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file has the primary job of showing and editing people's profiles.
5
 * It also allows the user to change some of their or another's preferences,
6
 * and such things.
7
 *
8
 * @package   ElkArte Forum
9
 * @copyright ElkArte Forum contributors
10
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
11
 *
12
 * This file contains code covered by:
13
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
14
 *
15
 * @version 2.0 dev
16
 *
17
 */
18
19
namespace ElkArte\Profile;
20
21
use ElkArte\AbstractController;
22
use ElkArte\Action;
23
use ElkArte\Cache\Cache;
24
use ElkArte\Controller\Likes;
25
use ElkArte\EventManager;
26
use ElkArte\Exceptions\Exception;
27
use ElkArte\Helper\Util;
28
use ElkArte\Languages\Txt;
29
use ElkArte\Member;
30
use ElkArte\MembersList;
31
use ElkArte\Menu\Menu;
32
use ElkArte\User;
33
34
/**
35
 * Has the job of showing and editing people's profiles.
36
 */
37
class Profile extends AbstractController
38
{
39
	/** @var bool If the save was successful or not */
40
	private $completedSave = false;
41
42
	/** @var null If this was a request to save an update */
43
	private $isSaving;
44
45
	/** @var null What it says, on completion */
46
	private $_force_redirect;
47
48
	/** @var array|bool Holds the output of createMenu for the profile areas */
49
	private $_profile_include_data;
50
51
	/** @var string The current area chosen from the menu */
52
	private $_current_area;
53
54
	/** @var string The current subsection, if any, of the area chosen */
55
	private $_current_subsection;
56
57
	/** @var int Member id for the history being viewed */
58
	private $_memID = 0;
59
60
	/** @var Member The \ElkArte\Member object is stored here to avoid some global */
61
	private $_profile;
62
63
	/**
64
	 * Called before all other methods when coming from the dispatcher or
65
	 * action class.
66
	 */
67
	public function pre_dispatch()
68
	{
69
		require_once(SUBSDIR . '/Menu.subs.php');
70
		require_once(SUBSDIR . '/Profile.subs.php');
71
72
		$this->_memID = currentMemberID();
73
		MembersList::load($this->_memID, false, 'profile');
74
		$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...
75
	}
76
77
	/**
78
	 * Allow the change or view of profiles.
79
	 *
80
	 * - Fires the pre_load event
81
	 *
82
	 * @see AbstractController::action_index
83
	 */
84
	public function action_index()
85
	{
86
		global $txt, $context, $cur_profile, $profile_vars, $post_errors;
87
88
		// Don't reload this as we may have processed error strings.
89
		if (empty($post_errors))
90
		{
91
			Txt::load('Profile');
92
		}
93
94
		theme()->getTemplates()->load('Profile');
95
96
		// Trigger profile pre-load event
97
		$this->_events->trigger('pre_load', ['post_errors' => $post_errors]);
98
99
		// A little bit about this member
100
		$context['id_member'] = $this->_memID;
101
		$cur_profile = $this->_profile;
102
103
		// Let's have some information about this member ready, too.
104
		$context['member'] = $this->_profile;
105
		$context['member']->loadContext();
106
107
		// Is this their own profile or are they looking at someone else?
108
		$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...
109
110
		// Create the menu of profile options
111
		$this->_define_profile_menu();
112
113
		// Is there an updated message to show?
114
		if (isset($this->_req->query->updated))
115
		{
116
			$context['push_alert'] = $_SESSION['push_enabled'] ?? null;
117
			unset($_SESSION['push_enabled']);
118
			$context['profile_updated'] = $txt['profile_updated_own'];
119
		}
120
121
		// If it said no permissions that meant it wasn't valid!
122
		if (empty($this->_profile_include_data['permission']))
123
		{
124
			throw new Exception('no_access', false);
125
		}
126
127
		// Choose the right permission set and do a pat check for good measure.
128
		$this->_profile_include_data['permission'] = $this->_profile_include_data['permission'][$context['user']['is_owner'] ? 'own' : 'any'];
129
		isAllowedTo($this->_profile_include_data['permission']);
130
131
		// Set the selected item - now it's been validated.
132
		$this->_current_area = $this->_profile_include_data['current_area'];
133
		$this->_current_subsection = $this->_profile_include_data['current_subsection'] ?? '';
134
		$context['menu_item_selected'] = $this->_current_area;
135
136
		// Before we go any further, let's work on the area we've said is valid.
137
		// Note this is done here just in case we ever compromise the menu function in error!
138
		$context['do_preview'] = isset($this->_req->post->preview_signature);
139
		$this->isSaving = $this->_req->getRequest('save', 'trim');
140
141
		// Session validation and/or Token Checks
142
		$this->_check_access();
143
144
		// Build the link tree.
145
		$this->_build_profile_breadcrumbs();
146
147
		// Set the template for this area... if you still can :P
148
		// and add the profile layer.
149
		$context['sub_template'] = $this->_profile_include_data['function'];
150
		theme()->getLayers()->add('profile');
151
152
		// Need JS if we made it this far
153
		loadJavascriptFile('profile.js');
154
155
		// Right - are we saving - if so let's save the old data first.
156
		$this->_save_updates();
157
158
		// Have some errors for some reason?
159
		// @todo check that this can be safely removed.
160
		if (!empty($post_errors))
161
		{
162
			// Set all the errors so the template knows what went wrong.
163
			foreach ($post_errors as $error_type)
164
			{
165
				$context['modify_error'][$error_type] = true;
166
			}
167
		}
168
		// If it's you then we should redirect upon save.
169
		elseif (!empty($profile_vars) && $context['user']['is_owner'] && !$context['do_preview'])
170
		{
171
			redirectexit('action=profile;area=' . $this->_current_area . (empty($this->_current_subsection) ? '' : ';sa=' . $this->_current_subsection) . ';updated');
172
		}
173
		elseif (!empty($this->_force_redirect))
174
		{
175
			redirectexit('action=profile' . ($context['user']['is_owner'] ? '' : ';u=' . $this->_memID) . ';area=' . $this->_current_area);
176
		}
177
178
		// Set the page title if it's not already set...
179
		if (!isset($context['page_title']))
180
		{
181
			$context['page_title'] = $txt['profile'] . (isset($txt[$this->_current_area]) ? ' - ' . $txt[$this->_current_area] : '');
182
		}
183
184
		// And off we go,
185
		$action = new Action();
186
		$action->initialize(['action' => $this->_profile_include_data]);
187
		$action->dispatch('action');
188
	}
189
190
	/**
191
	 * Define all the sections within the profile area!
192
	 *
193
	 * We start by defining the permission required - then we take this and turn
194
	 * it into the relevant context ;)
195
	 *
196
	 * Possible fields:
197
	 *   For Section:
198
	 *    - string $title: Section title.
199
	 *    - array $areas:  Array of areas within this section.
200
	 *
201
	 *   For Areas:
202
	 *    - string $label:      Text string that will be used to show the area in the menu.
203
	 *    - string $file:       Optional text string that may contain a file name that's needed for inclusion in order to display the area properly.
204
	 *    - string $custom_url: Optional href for area.
205
	 *    - string $function:   Function to execute for this section.
206
	 *    - bool $enabled:      Should area be shown?
207
	 *    - string $sc:         Session check validation to do on save - note without this save will get unset - if set.
208
	 *    - bool $hidden:       Does this not actually appear on the menu?
209
	 *    - bool $password:     Whether to require the user's password in order to save the data in the area.
210
	 *    - array $subsections: Array of subsections, in order of appearance.
211
	 *    - array $permission:  Array of permissions to determine who can access this area. Should contain arrays $own and $any.
212
	 */
213
	private function _define_profile_menu()
214
	{
215
		global $txt, $context, $modSettings;
216
217
		$profile_areas = [
218
			'info' => [
219
				'title' => $txt['profileInfo'],
220
				'areas' => [
221
					'summary' => [
222
						'label' => $txt['summary'],
223
						'controller' => ProfileInfo::class,
224
						'function' => 'action_summary',
225
						// From the summary it's possible to activate an account, so we need the token
226
						'token' => 'profile-aa%u',
227
						'token_type' => 'get',
228
						'permission' => [
229
							'own' => 'profile_view_own',
230
							'any' => 'profile_view_any',
231
						],
232
					],
233
					'statistics' => [
234
						'label' => $txt['statPanel'],
235
						'controller' => ProfileInfo::class,
236
						'function' => 'action_statPanel',
237
						'permission' => [
238
							'own' => 'profile_view_own',
239
							'any' => 'profile_view_any',
240
						],
241
					],
242
					'showposts' => [
243
						'label' => $txt['showPosts'],
244
						'controller' => ProfileInfo::class,
245
						'function' => 'action_showPosts',
246
						'subsections' => [
247
							'messages' => [$txt['showMessages'], ['profile_view_own', 'profile_view_any']],
248
							'topics' => [$txt['showTopics'], ['profile_view_own', 'profile_view_any']],
249
							'unwatchedtopics' => [$txt['showUnwatched'], ['profile_view_own', 'profile_view_any'], 'enabled' => $modSettings['enable_unwatch'] && $context['user']['is_owner']],
250
							'attach' => [$txt['showAttachments'], ['profile_view_own', 'profile_view_any']],
251
						],
252
						'permission' => [
253
							'own' => 'profile_view_own',
254
							'any' => 'profile_view_any',
255
						],
256
					],
257
					'showlikes' => [
258
						'label' => $txt['likes_show'],
259
						'controller' => Likes::class,
260
						'function' => 'action_showProfileLikes',
261
						'enabled' => !empty($modSettings['likes_enabled']) && $context['user']['is_owner'],
262
						'subsections' => [
263
							'given' => [$txt['likes_given'], ['profile_view_own']],
264
							'received' => [$txt['likes_received'], ['profile_view_own']],
265
						],
266
						'permission' => [
267
							'own' => 'profile_view_own',
268
							'any' => [],
269
						],
270
					],
271
					'permissions' => [
272
						'label' => $txt['showPermissions'],
273
						'controller' => ProfileInfo::class,
274
						'function' => 'action_showPermissions',
275
						'permission' => [
276
							'own' => 'manage_permissions',
277
							'any' => 'manage_permissions',
278
						],
279
					],
280
					'history' => [
281
						'label' => $txt['history'],
282
						'controller' => ProfileHistory::class,
283
						'function' => 'action_index',
284
						'subsections' => [
285
							'activity' => [$txt['trackActivity'], 'moderate_forum'],
286
							'ip' => [$txt['trackIP'], 'moderate_forum'],
287
							'edits' => [$txt['trackEdits'], 'moderate_forum', 'enabled' => featureEnabled('ml') && !empty($modSettings['userlog_enabled'])],
288
							'logins' => [$txt['trackLogins'], ['profile_view_own', 'moderate_forum']],
289
						],
290
						'permission' => [
291
							'own' => 'moderate_forum',
292
							'any' => 'moderate_forum',
293
						],
294
					],
295
					'viewwarning' => [
296
						'label' => $txt['profile_view_warnings'],
297
						'enabled' => featureEnabled('w') && !empty($modSettings['warning_enable']) && $this->_profile['warning'] && (!empty($modSettings['warning_show']) && ($context['user']['is_owner'] || $modSettings['warning_show'] == 2)),
298
						'controller' => ProfileInfo::class,
299
						'function' => 'action_viewWarning',
300
						'permission' => [
301
							'own' => 'profile_view_own',
302
							'any' => 'issue_warning',
303
						],
304
					],
305
				],
306
			],
307
			'edit_profile' => [
308
				'title' => $txt['profileEdit'],
309
				'areas' => [
310
					'account' => [
311
						'label' => $txt['account'],
312
						'controller' => ProfileOptions::class,
313
						'function' => 'action_account',
314
						'enabled' => $context['user']['is_admin'] || ((int) $this->_profile['id_group'] !== 1 && !in_array(1, array_map('intval', explode(',', $this->_profile['additional_groups'])), true)), 'sc' => 'post',
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

314
						'enabled' => $context['user']['is_admin'] || ((int) $this->_profile['id_group'] !== 1 && !in_array(1, array_map('intval', explode(',', /** @scrutinizer ignore-type */ $this->_profile['additional_groups'])), true)), 'sc' => 'post',
Loading history...
315
						'token' => 'profile-ac%u',
316
						'password' => true,
317
						'permission' => [
318
							'own' => ['profile_identity_any', 'profile_identity_own', 'manage_membergroups'],
319
							'any' => ['profile_identity_any', 'manage_membergroups'],
320
						],
321
					],
322
					'forumprofile' => [
323
						'label' => $txt['forumprofile'],
324
						'controller' => ProfileOptions::class,
325
						'function' => 'action_forumProfile',
326
						'sc' => 'post',
327
						'token' => 'profile-fp%u',
328
						'permission' => [
329
							'own' => ['profile_extra_any', 'profile_extra_own', 'profile_title_own', 'profile_title_any'],
330
							'any' => ['profile_extra_any', 'profile_title_any'],
331
						],
332
					],
333
					'theme' => [
334
						'label' => $txt['theme'],
335
						'controller' => ProfileOptions::class,
336
						'function' => 'action_themepick',
337
						'sc' => 'post',
338
						'token' => 'profile-th%u',
339
						'permission' => [
340
							'own' => ['profile_extra_any', 'profile_extra_own'],
341
							'any' => ['profile_extra_any'],
342
						],
343
					],
344
					'pick' => [
345
						'label' => $txt['theme'],
346
						'controller' => ProfileOptions::class,
347
						'function' => 'action_pick',
348
						'hidden' => true,
349
						'sc' => 'post',
350
						'token' => 'profile-th%u',
351
						'permission' => [
352
							'own' => ['profile_extra_any', 'profile_extra_own'],
353
							'any' => ['profile_extra_any'],
354
						],
355
					],
356
					'notification' => [
357
						'label' => $txt['notifications'],
358
						'controller' => ProfileOptions::class,
359
						'function' => 'action_notification',
360
						'sc' => 'post',
361
						'token' => 'profile-nt%u',
362
						'subsections' => [
363
							'settings' => [$txt['notify_settings']],
364
							'boards' => [$txt['notify_boards']],
365
							'topics' => [$txt['notify_topics']],
366
						],
367
						'permission' => [
368
							'own' => ['profile_extra_any', 'profile_extra_own'],
369
							'any' => ['profile_extra_any'],
370
						],
371
					],
372
					// Without profile_extra_own, settings are accessible from the PM section.
373
					// @todo at some point decouple it from PMs
374
					'contactprefs' => [
375
						'label' => $txt['contactprefs'],
376
						'controller' => ProfileOptions::class,
377
						'function' => 'action_pmprefs',
378
						'enabled' => allowedTo(['profile_extra_own', 'profile_extra_any']),
379
						'sc' => 'post',
380
						'token' => 'profile-pm%u',
381
						'permission' => [
382
							'own' => ['pm_read'],
383
							'any' => ['profile_extra_any'],
384
						],
385
					],
386
					'ignoreboards' => [
387
						'label' => $txt['ignoreboards'],
388
						'controller' => ProfileOptions::class,
389
						'function' => 'action_ignoreboards',
390
						'enabled' => !empty($modSettings['allow_ignore_boards']),
391
						'sc' => 'post',
392
						'token' => 'profile-ib%u',
393
						'permission' => [
394
							'own' => ['profile_extra_any', 'profile_extra_own'],
395
							'any' => ['profile_extra_any'],
396
						],
397
					],
398
					'lists' => [
399
						'label' => $txt['editBuddyIgnoreLists'],
400
						'controller' => ProfileOptions::class,
401
						'function' => 'action_editBuddyIgnoreLists',
402
						'enabled' => !empty($modSettings['enable_buddylist']) && $context['user']['is_owner'],
403
						'sc' => 'post',
404
						'token' => 'profile-bl%u',
405
						'subsections' => [
406
							'buddies' => [$txt['editBuddies']],
407
							'ignore' => [$txt['editIgnoreList']],
408
						],
409
						'permission' => [
410
							'own' => ['profile_extra_any', 'profile_extra_own'],
411
							'any' => [],
412
						],
413
					],
414
					'groupmembership' => [
415
						'label' => $txt['groupmembership'],
416
						'controller' => ProfileOptions::class,
417
						'function' => 'action_groupMembership',
418
						'enabled' => !empty($modSettings['show_group_membership']) && $context['user']['is_owner'],
419
						'sc' => 'request',
420
						'token' => 'profile-gm%u',
421
						'token_type' => 'request',
422
						'permission' => [
423
							'own' => ['profile_view_own'],
424
							'any' => ['manage_membergroups'],
425
						],
426
					],
427
				],
428
			],
429
			'profile_action' => [
430
				'title' => $txt['profileAction'],
431
				'areas' => [
432
					'sendpm' => [
433
						'label' => $txt['profileSendIm'],
434
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
435
						'permission' => [
436
							'own' => [],
437
							'any' => ['pm_send'],
438
						],
439
					],
440
					'issuewarning' => [
441
						'label' => $txt['profile_issue_warning'],
442
						'enabled' => featureEnabled('w') && !empty($modSettings['warning_enable']) && (!$context['user']['is_owner'] || $context['user']['is_admin']),
443
						'controller' => ProfileAccount::class,
444
						'function' => 'action_issuewarning',
445
						'token' => 'profile-iw%u',
446
						'permission' => [
447
							'own' => [],
448
							'any' => ['issue_warning'],
449
						],
450
					],
451
					'banuser' => [
452
						'label' => $txt['profileBanUser'],
453
						'custom_url' => getUrl('admin', ['action' => 'admin', 'area' => 'ban', 'sa' => 'add']),
454
						'enabled' => (int) $this->_profile['id_group'] !== 1 && !in_array(1, array_map('intval', explode(',', $this->_profile['additional_groups'])), true),
455
						'permission' => [
456
							'own' => [],
457
							'any' => ['manage_bans'],
458
						],
459
					],
460
					'subscriptions' => [
461
						'label' => $txt['subscriptions'],
462
						'controller' => ProfileSubscriptions::class,
463
						'function' => 'action_subscriptions',
464
						'enabled' => !empty($modSettings['paid_enabled']),
465
						'permission' => [
466
							'own' => ['profile_view_own'],
467
							'any' => ['moderate_forum'],
468
						],
469
					],
470
					'deleteaccount' => [
471
						'label' => $txt['deleteAccount'],
472
						'controller' => ProfileAccount::class,
473
						'function' => 'action_deleteaccount',
474
						'sc' => 'post',
475
						'token' => 'profile-da%u',
476
						'password' => true,
477
						'permission' => [
478
							'own' => ['profile_remove_any', 'profile_remove_own'],
479
							'any' => ['profile_remove_any'],
480
						],
481
					],
482
					'activateaccount' => [
483
						'controller' => ProfileAccount::class,
484
						'function' => 'action_activateaccount',
485
						'sc' => 'get',
486
						'token' => 'profile-aa%u',
487
						'token_type' => 'get',
488
						'permission' => [
489
							'own' => [],
490
							'any' => ['moderate_forum'],
491
						],
492
					],
493
				],
494
			],
495
		];
496
497
		// Set a few options for the menu.
498
		$menuOptions = [
499
			'disable_url_session_check' => true,
500
			'hook' => 'profile',
501
			'extra_url_parameters' => [
502
				'u' => $context['id_member'],
503
			],
504
		];
505
506
		// Actually create the menu!
507
		$this->_profile_include_data = (new Menu())
508
			->addMenuData($profile_areas)
509
			->addOptions($menuOptions)
510
			->prepareMenu()
511
			->setContext()
512
			->getIncludeData();
513
514
		unset($profile_areas);
515
516
		// Make a note of the Unique ID for this menu.
517
		$context['profile_menu_id'] = $context['max_menu_id'];
518
		$context['profile_menu_name'] = 'menu_data_' . $context['profile_menu_id'];
519
	}
520
521
	/**
522
	 * Does session and token checks for the areas that require those
523
	 */
524
	private function _check_access()
525
	{
526
		global $context;
527
528
		// Check the session, if required, and they are trying to save
529
		$this->completedSave = false;
530
		if (isset($this->_profile_include_data['sc']) && ($this->isSaving !== null || $context['do_preview']))
531
		{
532
			checkSession($this->_profile_include_data['sc']);
533
			$this->completedSave = true;
534
		}
535
536
		// Does this require admin/moderator session validating?
537
		if ($this->isSaving !== null && !$context['user']['is_owner'])
538
		{
539
			validateSession();
540
		}
541
542
		// Do we need to perform a token check?
543
		if (!empty($this->_profile_include_data['token']))
544
		{
545
			$token_name = str_replace('%u', $context['id_member'], $this->_profile_include_data['token']);
546
			$token_type = $this->_profile_include_data['tokenType'] ?? 'post';
547
548
			if (!in_array($token_type, ['request', 'post', 'get']))
549
			{
550
				$token_type = 'post';
551
			}
552
553
			if ($this->isSaving !== null)
554
			{
555
				validateToken($token_name, $token_type);
556
			}
557
558
			createToken($token_name, $token_type);
559
			$context['token_check'] = $token_name;
560
		}
561
	}
562
563
	/**
564
	 * Just builds the link tree based on where were are in the profile section
565
	 * and whose profile is being viewed, etc.
566
	 */
567
	private function _build_profile_breadcrumbs()
568
	{
569
		global $context, $txt;
570
571
		$context['breadcrumbs'][] = [
572
			'url' => getUrl('profile', ['action' => 'profile', 'u' => $this->_memID, 'name' => $this->_profile['real_name']]),
573
			'name' => sprintf($txt['profile_of_username'], $context['member']['name']),
574
		];
575
576
		if (!empty($this->_profile_include_data['label']))
577
		{
578
			$context['breadcrumbs'][] = [
579
				'url' => getUrl('profile', ['action' => 'profile', 'area' => $this->_profile_include_data['current_area'], 'u' => $this->_memID, 'name' => $this->_profile['real_name']]),
580
				'name' => $this->_profile_include_data['label'],
581
			];
582
		}
583
584
		if (empty($this->_current_subsection))
585
		{
586
			return;
587
		}
588
589
		if (!isset($this->_profile_include_data['subsections'][$this->_current_subsection]))
590
		{
591
			return;
592
		}
593
594
		if ($this->_profile_include_data['subsections'][$this->_current_subsection]['label'] === $this->_profile_include_data['label'])
595
		{
596
			return;
597
		}
598
599
		$context['breadcrumbs'][] = [
600
			'url' => getUrl('profile', ['action' => 'profile', 'area' => $this->_profile_include_data['current_area'], 'sa' => $this->_current_subsection, 'u' => $this->_memID, 'name' => $this->_profile['real_name']]),
601
			'name' => $this->_profile_include_data['subsections'][$this->_current_subsection]['label'],
602
		];
603
	}
604
605
	/**
606
	 * Save profile updates
607
	 */
608
	private function _save_updates()
609
	{
610
		global $txt, $context, $modSettings, $post_errors, $profile_vars;
611
612
		// All the subActions that require a user password in order to validate.
613
		$check_password = $context['user']['is_owner'] && !empty($this->_profile_include_data['password']);
614
		$context['require_password'] = $check_password;
615
616
		// These will get populated soon!
617
		$post_errors = [];
618
		$profile_vars = [];
619
620
		if ($this->completedSave)
621
		{
622
			// Clean up the POST variables.
623
			$post = Util::htmltrim__recursive((array) $this->_req->post);
624
			$post = Util::htmlspecialchars__recursive($post);
625
			$this->_req->post = new \ArrayObject($post, \ArrayObject::ARRAY_AS_PROPS);
0 ignored issues
show
Bug introduced by
It seems like $post can also be of type string; however, parameter $array of ArrayObject::__construct() does only seem to accept array|object, 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

625
			$this->_req->post = new \ArrayObject(/** @scrutinizer ignore-type */ $post, \ArrayObject::ARRAY_AS_PROPS);
Loading history...
626
627
			// Does the change require the current password as well?
628
			$this->_check_password($check_password);
629
630
			// Change the IP address in the database.
631
			if ($context['user']['is_owner'])
632
			{
633
				$profile_vars['member_ip'] = $this->user->ip;
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
634
			}
635
636
			// Now call the sub-action function...
637
			if ($this->_current_area === 'activateaccount' && empty($post_errors))
638
			{
639
				$controller = new ProfileAccount(new EventManager());
640
				$controller->setUser(User::$info);
641
				$controller->pre_dispatch();
642
				$controller->action_activateaccount();
643
			}
644
			elseif ($this->_current_area === 'deleteaccount' && empty($post_errors))
645
			{
646
				$controller = new ProfileAccount(new EventManager());
647
				$controller->setUser(User::$info);
648
				$controller->pre_dispatch();
649
				$controller->action_deleteaccount2();
650
651
				// Done
652
				redirectexit();
653
			}
654
			elseif ($this->_current_area === 'groupmembership' && empty($post_errors))
655
			{
656
				$controller = new ProfileOptions(new EventManager());
657
				$controller->setUser(User::$info);
658
				$controller->pre_dispatch();
659
				$msg = $controller->action_groupMembership2();
660
661
				// Whatever we've done, we have nothing else to do here...
662
				redirectexit('action=profile' . ($context['user']['is_owner'] ? '' : ';u=' . $this->_memID) . ';area=groupmembership' . (empty($msg) ? '' : ';msg=' . $msg));
663
			}
664
			// Authentication changes?
665
			elseif ($this->_current_area === 'authentication')
666
			{
667
				$controller = new ProfileOptions(new EventManager());
668
				$controller->setUser(User::$info);
669
				$controller->pre_dispatch();
670
				$controller->action_authentication(true);
0 ignored issues
show
Bug introduced by
The method action_authentication() does not exist on ElkArte\Profile\ProfileOptions. ( Ignorable by Annotation )

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

670
				$controller->/** @scrutinizer ignore-call */ 
671
                 action_authentication(true);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
671
			}
672
			elseif (in_array($this->_current_area, ['account', 'forumprofile', 'theme', 'contactprefs']))
673
			{
674
				// @todo yes this is ugly, but saveProfileFields needs to be updated first
675
				$_POST = (array) $this->_req->post;
676
677
				if ($this->_current_area === 'account' && !empty($modSettings['enableOTP']))
678
				{
679
					$fields = ProfileOptions::getFields('account_otp');
680
				}
681
				else
682
				{
683
					$fields = ProfileOptions::getFields($this->_current_area);
684
				}
685
686
				$profileFields = new ProfileFields();
687
				$profileFields->saveProfileFields($fields['fields'], $fields['hook']);
688
			}
689
			elseif (empty($post_errors))
690
			{
691
				// @todo yes this is also ugly, but saveProfileChanges needs to be updated first
692
				$_POST = (array) $this->_req->post;
693
694
				$this->_force_redirect = true;
0 ignored issues
show
Documentation Bug introduced by
It seems like true of type true is incompatible with the declared type null of property $_force_redirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
695
				saveProfileChanges($profile_vars, $this->_memID);
696
			}
697
698
			call_integration_hook('integrate_profile_save', [&$profile_vars, &$post_errors, $this->_memID]);
699
700
			// There was a problem, let them try to re-enter.
701
			if (!empty($post_errors))
702
			{
703
				// Load the language file so we can give a nice explanation of the errors.
704
				Txt::load('Errors');
705
				$context['post_errors'] = $post_errors;
706
			}
707
			elseif (!empty($profile_vars))
708
			{
709
				// If we've changed the password, notify any integration that may be listening in.
710
				if (isset($profile_vars['passwd']))
711
				{
712
					call_integration_hook('integrate_reset_pass', [$this->_profile['member_name'], $this->_profile['member_name'], $this->_req->post->passwrd2]);
0 ignored issues
show
Bug introduced by
The property passwrd2 does not seem to exist on ArrayObject.
Loading history...
713
				}
714
715
				require_once(SUBSDIR . '/Members.subs.php');
716
				updateMemberData($this->_memID, $profile_vars);
717
718
				// What if this is the newest member?
719
				if ((int) $modSettings['latestMember'] === $this->_memID)
720
				{
721
					require_once(SUBSDIR . '/Members.subs.php');
722
					updateMemberStats();
723
				}
724
				elseif (isset($profile_vars['real_name']))
725
				{
726
					updateSettings(['memberlist_updated' => time()]);
727
				}
728
729
				// If the member changed his/her birth date, update calendar statistics.
730
				if (isset($profile_vars['birthdate']) || isset($profile_vars['real_name']))
731
				{
732
					updateSettings([
733
						'calendar_updated' => time(),
734
					]);
735
				}
736
737
				// Anything worth logging?
738
				if (!empty($context['log_changes']) && !empty($modSettings['userlog_enabled']) && featureEnabled('ml'))
739
				{
740
					$log_changes = [];
741
					foreach ($context['log_changes'] as $k => $v)
742
					{
743
						$log_changes[] = [
744
							'action' => $k,
745
							'log_type' => 'user',
746
							'extra' => array_merge($v, [
747
								'applicator' => $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...
748
								'member_affected' => $this->_memID,
749
							]),
750
						];
751
					}
752
753
					logActions($log_changes);
754
				}
755
756
				// Have we got any post save functions to execute?
757
				if (!empty($context['profile_execute_on_save']))
758
				{
759
					foreach ($context['profile_execute_on_save'] as $saveFunc)
760
					{
761
						$saveFunc();
762
					}
763
				}
764
765
				// Let them know it worked!
766
				$context['profile_updated'] = $context['user']['is_owner'] ? $txt['profile_updated_own'] : sprintf($txt['profile_updated_else'], $this->_profile['member_name']);
767
768
				// Invalidate any cached data.
769
				Cache::instance()->remove('member_data-profile-' . $this->_memID);
770
				MembersList::load($this->_memID, false, 'profile');
771
			}
772
		}
773
	}
774
775
	/**
776
	 * If a password validation before a change is needed, this is the function to do it
777
	 *
778
	 * @param bool $check_password if this profile update requires a password verification
779
	 * @throws Exception
780
	 */
781
	private function _check_password($check_password)
782
	{
783
		global $post_errors, $context;
784
785
		if ($check_password)
786
		{
787
			// You didn't even enter a password!
788
			if (trim($this->_req->post->oldpasswrd) === '')
789
			{
790
				$post_errors[] = 'no_password';
791
			}
792
793
			// Since the password got modified due to all the $_POST cleaning, lets undo it so we can get the correct password
794
			$this->_req->post->oldpasswrd = un_htmlspecialchars($this->_req->post->oldpasswrd);
795
796
			// Does the integration want to check passwords?
797
			$good_password = in_array(true, call_integration_hook('integrate_verify_password', [$this->_profile['member_name'], $this->_req->post->oldpasswrd, false]), true);
798
799
			// Start up the password checker, we have work to do
800
			require_once(SUBSDIR . '/Auth.subs.php');
801
802
			// Bad password!!!
803
			if (!$good_password && !validateLoginPassword($this->_req->post->oldpasswrd, $this->user->passwd, $this->_profile['member_name']))
0 ignored issues
show
Bug Best Practice introduced by
The property passwd does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
804
			{
805
				$post_errors[] = 'bad_password';
806
			}
807
808
			// Warn other elements not to jump the gun and do custom changes!
809
			if (in_array('bad_password', $post_errors, true))
810
			{
811
				$context['password_auth_failed'] = true;
812
			}
813
		}
814
	}
815
}
816