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); |
|
|
|
|
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; |
|
|
|
|
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', |
|
|
|
|
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); |
|
|
|
|
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; |
|
|
|
|
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); |
|
|
|
|
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; |
|
|
|
|
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]); |
|
|
|
|
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, |
|
|
|
|
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'])) |
|
|
|
|
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
|
|
|
|
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 theid
property of an instance of theAccount
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.