Completed
Pull Request — release-2.1 (#5072)
by John
06:28
created

Profile-Modify.php ➔ tfasetup()   C

Complexity

Conditions 15
Paths 21

Size

Total Lines 73

Duplication

Lines 2
Ratio 2.74 %

Importance

Changes 0
Metric Value
cc 15
nc 21
nop 1
dl 2
loc 73
rs 5.3224
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines http://www.simplemachines.org
12
 * @copyright 2018 Simple Machines and individual contributors
13
 * @license http://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1 Beta 4
16
 */
17
18
if (!defined('SMF'))
19
	die('No direct access...');
20
21
/**
22
 * This defines every profile field known to man.
23
 *
24
 * @param bool $force_reload Whether to reload the data
25
 */
26
function loadProfileFields($force_reload = false)
27
{
28
	global $context, $profile_fields, $txt, $scripturl, $modSettings, $user_info, $smcFunc, $cur_profile, $language;
29
	global $sourcedir, $profile_vars;
30
31
	// Don't load this twice!
32
	if (!empty($profile_fields) && !$force_reload)
33
		return;
34
35
	/* This horrific array defines all the profile fields in the whole world!
36
		In general each "field" has one array - the key of which is the database column name associated with said field. Each item
37
		can have the following attributes:
38
39
				string $type:			The type of field this is - valid types are:
40
					- callback:		This is a field which has its own callback mechanism for templating.
41
					- check:		A simple checkbox.
42
					- hidden:		This doesn't have any visual aspects but may have some validity.
43
					- password:		A password box.
44
					- select:		A select box.
45
					- text:			A string of some description.
46
47
				string $label:			The label for this item - default will be $txt[$key] if this isn't set.
48
				string $subtext:		The subtext (Small label) for this item.
49
				int $size:			Optional size for a text area.
50
				array $input_attr:		An array of text strings to be added to the input box for this item.
51
				string $value:			The value of the item. If not set $cur_profile[$key] is assumed.
52
				string $permission:		Permission required for this item (Excluded _any/_own subfix which is applied automatically).
53
				function $input_validate:	A runtime function which validates the element before going to the database. It is passed
54
								the relevant $_POST element if it exists and should be treated like a reference.
55
56
								Return types:
57
					- true:			Element can be stored.
58
					- false:		Skip this element.
59
					- a text string:	An error occured - this is the error message.
60
61
				function $preload:		A function that is used to load data required for this element to be displayed. Must return
62
								true to be displayed at all.
63
64
				string $cast_type:		If set casts the element to a certain type. Valid types (bool, int, float).
65
				string $save_key:		If the index of this element isn't the database column name it can be overriden
66
								with this string.
67
				bool $is_dummy:			If set then nothing is acted upon for this element.
68
				bool $enabled:			A test to determine whether this is even available - if not is unset.
69
				string $link_with:		Key which links this field to an overall set.
70
71
		Note that all elements that have a custom input_validate must ensure they set the value of $cur_profile correct to enable
72
		the changes to be displayed correctly on submit of the form.
73
74
	*/
75
76
	$profile_fields = array(
77
		'avatar_choice' => array(
78
			'type' => 'callback',
79
			'callback_func' => 'avatar_select',
80
			// This handles the permissions too.
81
			'preload' => 'profileLoadAvatarData',
82
			'input_validate' => 'profileSaveAvatarData',
83
			'save_key' => 'avatar',
84
		),
85
		'bday1' => array(
86
			'type' => 'callback',
87
			'callback_func' => 'birthdate',
88
			'permission' => 'profile_extra',
89
			'preload' => function() use ($cur_profile, &$context)
90
			{
91
				// Split up the birthdate....
92
				list ($uyear, $umonth, $uday) = explode('-', empty($cur_profile['birthdate']) || $cur_profile['birthdate'] === '1004-01-01' ? '--' : $cur_profile['birthdate']);
93
				$context['member']['birth_date'] = array(
94
					'year' => $uyear,
95
					'month' => $umonth,
96
					'day' => $uday,
97
				);
98
99
				return true;
100
			},
101
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
102
			{
103
				if (isset($_POST['bday2'], $_POST['bday3']) && $value > 0 && $_POST['bday2'] > 0)
104
				{
105
					// Set to blank?
106
					if ((int) $_POST['bday3'] == 1 && (int) $_POST['bday2'] == 1 && (int) $value == 1)
107
						$value = '1004-01-01';
108
					else
109
						$value = checkdate($value, $_POST['bday2'], $_POST['bday3'] < 1004 ? 1004 : $_POST['bday3']) ? sprintf('%04d-%02d-%02d', $_POST['bday3'] < 1004 ? 1004 : $_POST['bday3'], $_POST['bday1'], $_POST['bday2']) : '1004-01-01';
110
				}
111
				else
112
					$value = '1004-01-01';
113
114
				$profile_vars['birthdate'] = $value;
115
				$cur_profile['birthdate'] = $value;
116
				return false;
117
			},
118
		),
119
		// Setting the birthdate the old style way?
120
		'birthdate' => array(
121
			'type' => 'hidden',
122
			'permission' => 'profile_extra',
123
			'input_validate' => function(&$value) use ($cur_profile)
124
			{
125
				// @todo Should we check for this year and tell them they made a mistake :P? (based on coppa at least?)
126
				if (preg_match('/(\d{4})[\-\., ](\d{2})[\-\., ](\d{2})/', $value, $dates) === 1)
127
				{
128
					$value = checkdate($dates[2], $dates[3], $dates[1] < 4 ? 4 : $dates[1]) ? sprintf('%04d-%02d-%02d', $dates[1] < 4 ? 4 : $dates[1], $dates[2], $dates[3]) : '1004-01-01';
129
					return true;
130
				}
131
				else
132
				{
133
					$value = empty($cur_profile['birthdate']) ? '1004-01-01' : $cur_profile['birthdate'];
134
					return false;
135
				}
136
			},
137
		),
138
		'date_registered' => array(
139
			'type' => 'date',
140
			'value' => empty($cur_profile['date_registered']) ? $txt['not_applicable'] : strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600),
141
			'label' => $txt['date_registered'],
142
			'log_change' => true,
143
			'permission' => 'moderate_forum',
144
			'input_validate' => function(&$value) use ($txt, $user_info, $modSettings, $cur_profile, $context)
145
			{
146
				// Bad date!  Go try again - please?
147
				if (($value = strtotime($value)) === -1)
148
				{
149
					$value = $cur_profile['date_registered'];
150
					return $txt['invalid_registration'] . ' ' . strftime('%d %b %Y ' . (strpos($user_info['time_format'], '%H') !== false ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
151
				}
152
				// As long as it doesn't equal "N/A"...
153
				elseif ($value != $txt['not_applicable'] && $value != strtotime(strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600)))
154
					$value = $value - ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
155
				else
156
					$value = $cur_profile['date_registered'];
157
158
				return true;
159
			},
160
		),
161
		'email_address' => array(
162
			'type' => 'email',
163
			'label' => $txt['user_email_address'],
164
			'subtext' => $txt['valid_email'],
165
			'log_change' => true,
166
			'permission' => 'profile_password',
167
			'js_submit' => !empty($modSettings['send_validation_onChange']) ? '
168
	form_handle.addEventListener(\'submit\', function(event)
169
	{
170
		if (this.email_address.value != "'. (!empty($cur_profile['email_address']) ? $cur_profile['email_address'] : '') . '")
171
		{
172
			alert('. JavaScriptEscape($txt['email_change_logout']) . ');
173
			return true;
174
		}
175
	}, false);' : '',
176
			'input_validate' => function(&$value)
177
			{
178
				global $context, $old_profile, $profile_vars, $sourcedir, $modSettings;
179
180
				if (strtolower($value) == strtolower($old_profile['email_address']))
181
					return false;
182
183
				$isValid = profileValidateEmail($value, $context['id_member']);
184
185
				// Do they need to revalidate? If so schedule the function!
186
				if ($isValid === true && !empty($modSettings['send_validation_onChange']) && !allowedTo('moderate_forum'))
187
				{
188
					require_once($sourcedir . '/Subs-Members.php');
189
					$profile_vars['validation_code'] = generateValidationCode();
190
					$profile_vars['is_activated'] = 2;
191
					$context['profile_execute_on_save'][] = 'profileSendActivation';
192
					unset($context['profile_execute_on_save']['reload_user']);
193
				}
194
195
				return $isValid;
196
			},
197
		),
198
		// Selecting group membership is a complicated one so we treat it separate!
199
		'id_group' => array(
200
			'type' => 'callback',
201
			'callback_func' => 'group_manage',
202
			'permission' => 'manage_membergroups',
203
			'preload' => 'profileLoadGroups',
204
			'log_change' => true,
205
			'input_validate' => 'profileSaveGroups',
206
		),
207
		'id_theme' => array(
208
			'type' => 'callback',
209
			'callback_func' => 'theme_pick',
210
			'permission' => 'profile_extra',
211
			'enabled' => $modSettings['theme_allow'] || allowedTo('admin_forum'),
212
			'preload' => function() use ($smcFunc, &$context, $cur_profile, $txt)
213
			{
214
				$request = $smcFunc['db_query']('', '
215
					SELECT value
216
					FROM {db_prefix}themes
217
					WHERE id_theme = {int:id_theme}
218
						AND variable = {string:variable}
219
					LIMIT 1', array(
220
						'id_theme' => $cur_profile['id_theme'],
221
						'variable' => 'name',
222
					)
223
				);
224
				list ($name) = $smcFunc['db_fetch_row']($request);
225
				$smcFunc['db_free_result']($request);
226
227
				$context['member']['theme'] = array(
228
					'id' => $cur_profile['id_theme'],
229
					'name' => empty($cur_profile['id_theme']) ? $txt['theme_forum_default'] : $name
230
				);
231
				return true;
232
			},
233
			'input_validate' => function(&$value)
234
			{
235
				$value = (int) $value;
236
				return true;
237
			},
238
		),
239
		'lngfile' => array(
240
			'type' => 'select',
241
			'options' => function() use (&$context)
242
			{
243
				return $context['profile_languages'];
244
			},
245
			'label' => $txt['preferred_language'],
246
			'permission' => 'profile_identity',
247
			'preload' => 'profileLoadLanguages',
248
			'enabled' => !empty($modSettings['userLanguage']),
249
			'value' => empty($cur_profile['lngfile']) ? $language : $cur_profile['lngfile'],
250
			'input_validate' => function(&$value) use (&$context, $cur_profile)
251
			{
252
				// Load the languages.
253
				profileLoadLanguages();
254
255
				if (isset($context['profile_languages'][$value]))
256
				{
257
					if ($context['user']['is_owner'] && empty($context['password_auth_failed']))
258
						$_SESSION['language'] = $value;
259
					return true;
260
				}
261
				else
262
				{
263
					$value = $cur_profile['lngfile'];
264
					return false;
265
				}
266
			},
267
		),
268
		// The username is not always editable - so adjust it as such.
269
		'member_name' => array(
270
			'type' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? 'text' : 'label',
271
			'label' => $txt['username'],
272
			'subtext' => allowedTo('admin_forum') && !isset($_GET['changeusername']) ? '[<a href="' . $scripturl . '?action=profile;u=' . $context['id_member'] . ';area=account;changeusername" style="font-style: italic;">' . $txt['username_change'] . '</a>]' : '',
273
			'log_change' => true,
274
			'permission' => 'profile_identity',
275
			'prehtml' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? '<div class="alert">' . $txt['username_warning'] . '</div>' : '',
276
			'input_validate' => function(&$value) use ($sourcedir, $context, $user_info, $cur_profile)
277
			{
278
				if (allowedTo('admin_forum'))
279
				{
280
					// We'll need this...
281
					require_once($sourcedir . '/Subs-Auth.php');
282
283
					// Maybe they are trying to change their password as well?
284
					$resetPassword = true;
285
					if (isset($_POST['passwrd1']) && $_POST['passwrd1'] != '' && isset($_POST['passwrd2']) && $_POST['passwrd1'] == $_POST['passwrd2'] && validatePassword($_POST['passwrd1'], $value, array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email'])) == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing validatePassword($_POST[..., $user_info['email'])) of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
286
						$resetPassword = false;
287
288
					// Do the reset... this will send them an email too.
289
					if ($resetPassword)
290
						resetPassword($context['id_member'], $value);
291
					elseif ($value !== null)
292
					{
293
						validateUsername($context['id_member'], trim(preg_replace('~[\t\n\r \x0B\0' . ($context['utf8'] ? '\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}' : '\x00-\x08\x0B\x0C\x0E-\x19\xA0') . ']+~' . ($context['utf8'] ? 'u' : ''), ' ', $value)));
294
						updateMemberData($context['id_member'], array('member_name' => $value));
295
296
						// Call this here so any integrated systems will know about the name change (resetPassword() takes care of this if we're letting SMF generate the password)
297
						call_integration_hook('integrate_reset_pass', array($cur_profile['member_name'], $value, $_POST['passwrd1']));
298
					}
299
				}
300
				return false;
301
			},
302
		),
303
		'passwrd1' => array(
304
			'type' => 'password',
305
			'label' => ucwords($txt['choose_pass']),
306
			'subtext' => $txt['password_strength'],
307
			'size' => 20,
308
			'value' => '',
309
			'permission' => 'profile_password',
310
			'save_key' => 'passwd',
311
			// Note this will only work if passwrd2 also exists!
312
			'input_validate' => function(&$value) use ($sourcedir, $user_info, $smcFunc, $cur_profile)
313
			{
314
				// If we didn't try it then ignore it!
315
				if ($value == '')
316
					return false;
317
318
				// Do the two entries for the password even match?
319
				if (!isset($_POST['passwrd2']) || $value != $_POST['passwrd2'])
320
					return 'bad_new_password';
321
322
				// Let's get the validation function into play...
323
				require_once($sourcedir . '/Subs-Auth.php');
324
				$passwordErrors = validatePassword($value, $cur_profile['member_name'], array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email']));
325
326
				// Were there errors?
327
				if ($passwordErrors != null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $passwordErrors of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
328
					return 'password_' . $passwordErrors;
329
330
				// Set up the new password variable... ready for storage.
331
				$value = hash_password($cur_profile['member_name'], un_htmlspecialchars($value));
332
333
				return true;
334
			},
335
		),
336
		'passwrd2' => array(
337
			'type' => 'password',
338
			'label' => ucwords($txt['verify_pass']),
339
			'size' => 20,
340
			'value' => '',
341
			'permission' => 'profile_password',
342
			'is_dummy' => true,
343
		),
344
		'personal_text' => array(
345
			'type' => 'text',
346
			'label' => $txt['personal_text'],
347
			'log_change' => true,
348
			'input_attr' => array('maxlength="50"'),
349
			'size' => 50,
350
			'permission' => 'profile_blurb',
351
			'input_validate' => function(&$value) use ($smcFunc)
352
			{
353
				if ($smcFunc['strlen']($value) > 50)
354
					return 'personal_text_too_long';
355
356
				return true;
357
			},
358
		),
359
		// This does ALL the pm settings
360
		'pm_prefs' => array(
361
			'type' => 'callback',
362
			'callback_func' => 'pm_settings',
363
			'permission' => 'pm_read',
364
			'preload' => function() use (&$context, $cur_profile)
365
			{
366
				$context['display_mode'] = $cur_profile['pm_prefs'] & 3;
367
				$context['receive_from'] = !empty($cur_profile['pm_receive_from']) ? $cur_profile['pm_receive_from'] : 0;
368
369
				return true;
370
			},
371
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
372
			{
373
				// Simple validate and apply the two "sub settings"
374
				$value = max(min($value, 2), 0);
375
376
				$cur_profile['pm_receive_from'] = $profile_vars['pm_receive_from'] = max(min((int) $_POST['pm_receive_from'], 4), 0);
377
378
				return true;
379
			},
380
		),
381
		'posts' => array(
382
			'type' => 'int',
383
			'label' => $txt['profile_posts'],
384
			'log_change' => true,
385
			'size' => 7,
386
			'permission' => 'moderate_forum',
387
			'input_validate' => function(&$value)
388
			{
389
				if (!is_numeric($value))
390
					return 'digits_only';
391
				else
392
					$value = $value != '' ? strtr($value, array(',' => '', '.' => '', ' ' => '')) : 0;
393
				return true;
394
			},
395
		),
396
		'real_name' => array(
397
			'type' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum') ? 'text' : 'label',
398
			'label' => $txt['name'],
399
			'subtext' => $txt['display_name_desc'],
400
			'log_change' => true,
401
			'input_attr' => array('maxlength="60"'),
402
			'permission' => 'profile_displayed_name',
403
			'enabled' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum'),
404
			'input_validate' => function(&$value) use ($context, $smcFunc, $sourcedir, $cur_profile)
405
			{
406
				$value = trim(preg_replace('~[\t\n\r \x0B\0' . ($context['utf8'] ? '\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}' : '\x00-\x08\x0B\x0C\x0E-\x19\xA0') . ']+~' . ($context['utf8'] ? 'u' : ''), ' ', $value));
407
408
				if (trim($value) == '')
409
					return 'no_name';
410
				elseif ($smcFunc['strlen']($value) > 60)
411
					return 'name_too_long';
412
				elseif ($cur_profile['real_name'] != $value)
413
				{
414
					require_once($sourcedir . '/Subs-Members.php');
415
					if (isReservedName($value, $context['id_member']))
416
						return 'name_taken';
417
				}
418
				return true;
419
			},
420
		),
421
		'secret_question' => array(
422
			'type' => 'text',
423
			'label' => $txt['secret_question'],
424
			'subtext' => $txt['secret_desc'],
425
			'size' => 50,
426
			'permission' => 'profile_password',
427
		),
428
		'secret_answer' => array(
429
			'type' => 'text',
430
			'label' => $txt['secret_answer'],
431
			'subtext' => $txt['secret_desc2'],
432
			'size' => 20,
433
			'postinput' => '<span class="smalltext"><a href="' . $scripturl . '?action=helpadmin;help=secret_why_blank" onclick="return reqOverlayDiv(this.href);"><span class="generic_icons help"></span> ' . $txt['secret_why_blank'] . '</a></span>',
434
			'value' => '',
435
			'permission' => 'profile_password',
436
			'input_validate' => function(&$value) use ($cur_profile)
437
			{
438
				$value = $value != '' ? hash_password($cur_profile['member_name'], $value) : '';
439
				return true;
440
			},
441
		),
442
		'signature' => array(
443
			'type' => 'callback',
444
			'callback_func' => 'signature_modify',
445
			'permission' => 'profile_signature',
446
			'enabled' => substr($modSettings['signature_settings'], 0, 1) == 1,
447
			'preload' => 'profileLoadSignatureData',
448
			'input_validate' => 'profileValidateSignature',
449
		),
450
		'show_online' => array(
451
			'type' => 'check',
452
			'label' => $txt['show_online'],
453
			'permission' => 'profile_identity',
454
			'enabled' => !empty($modSettings['allow_hideOnline']) || allowedTo('moderate_forum'),
455
		),
456
		'smiley_set' => array(
457
			'type' => 'callback',
458
			'callback_func' => 'smiley_pick',
459
			'enabled' => !empty($modSettings['smiley_sets_enable']),
460
			'permission' => 'profile_extra',
461
			'preload' => function() use ($modSettings, &$context, $txt, $cur_profile, $smcFunc)
462
			{
463
				$context['member']['smiley_set']['id'] = empty($cur_profile['smiley_set']) ? '' : $cur_profile['smiley_set'];
464
				$context['smiley_sets'] = explode(',', 'none,,' . $modSettings['smiley_sets_known']);
465
				$set_names = explode("\n", $txt['smileys_none'] . "\n" . $txt['smileys_forum_board_default'] . "\n" . $modSettings['smiley_sets_names']);
466
				foreach ($context['smiley_sets'] as $i => $set)
467
				{
468
					$context['smiley_sets'][$i] = array(
469
						'id' => $smcFunc['htmlspecialchars']($set),
470
						'name' => $smcFunc['htmlspecialchars']($set_names[$i]),
471
						'selected' => $set == $context['member']['smiley_set']['id']
472
					);
473
474
					if ($context['smiley_sets'][$i]['selected'])
475
						$context['member']['smiley_set']['name'] = $set_names[$i];
476
				}
477
				return true;
478
			},
479
			'input_validate' => function(&$value)
480
			{
481
				global $modSettings;
482
483
				$smiley_sets = explode(',', $modSettings['smiley_sets_known']);
484
				if (!in_array($value, $smiley_sets) && $value != 'none')
485
					$value = '';
486
				return true;
487
			},
488
		),
489
		// Pretty much a dummy entry - it populates all the theme settings.
490
		'theme_settings' => array(
491
			'type' => 'callback',
492
			'callback_func' => 'theme_settings',
493
			'permission' => 'profile_extra',
494
			'is_dummy' => true,
495
			'preload' => function() use (&$context, $user_info, $modSettings)
496
			{
497
				loadLanguage('Settings');
498
499
				$context['allow_no_censored'] = false;
500
				if ($user_info['is_admin'] || $context['user']['is_owner'])
501
					$context['allow_no_censored'] = !empty($modSettings['allow_no_censored']);
502
503
				return true;
504
			},
505
		),
506
		'tfa' => array(
507
			'type' => 'callback',
508
			'callback_func' => 'tfa',
509
			'permission' => 'profile_password',
510
			'enabled' => !empty($modSettings['tfa_mode']),
511
			'preload' => function() use (&$context, $cur_profile)
512
			{
513
				$context['tfa_enabled'] = !empty($cur_profile['tfa_secret']);
514
515
				return true;
516
			},
517
		),
518
		'time_format' => array(
519
			'type' => 'callback',
520
			'callback_func' => 'timeformat_modify',
521
			'permission' => 'profile_extra',
522
			'preload' => function() use (&$context, $user_info, $txt, $cur_profile, $modSettings)
523
			{
524
				$context['easy_timeformats'] = array(
525
					array('format' => '', 'title' => $txt['timeformat_default']),
526
					array('format' => '%B %d, %Y, %I:%M:%S %p', 'title' => $txt['timeformat_easy1']),
527
					array('format' => '%B %d, %Y, %H:%M:%S', 'title' => $txt['timeformat_easy2']),
528
					array('format' => '%Y-%m-%d, %H:%M:%S', 'title' => $txt['timeformat_easy3']),
529
					array('format' => '%d %B %Y, %H:%M:%S', 'title' => $txt['timeformat_easy4']),
530
					array('format' => '%d-%m-%Y, %H:%M:%S', 'title' => $txt['timeformat_easy5'])
531
				);
532
533
				$context['member']['time_format'] = $cur_profile['time_format'];
534
				$context['current_forum_time'] = timeformat(time() - $user_info['time_offset'] * 3600, false);
535
				$context['current_forum_time_js'] = strftime('%Y,' . ((int) strftime('%m', time() + $modSettings['time_offset'] * 3600) - 1) . ',%d,%H,%M,%S', time() + $modSettings['time_offset'] * 3600);
536
				$context['current_forum_time_hour'] = (int) strftime('%H', forum_time(false));
537
				return true;
538
			},
539
		),
540
		'timezone' => array(
541
			'type' => 'select',
542
			'options' => smf_list_timezones(),
543
			'permission' => 'profile_extra',
544
			'label' => $txt['timezone'],
545
			'input_validate' => function($value)
546
			{
547
				$tz = smf_list_timezones();
548
				if (!isset($tz[$value]))
549
					return 'bad_timezone';
550
551
				return true;
552
			},
553
		),
554
		'usertitle' => array(
555
			'type' => 'text',
556
			'label' => $txt['custom_title'],
557
			'log_change' => true,
558
			'input_attr' => array('maxlength="50"'),
559
			'size' => 50,
560
			'permission' => 'profile_title',
561
			'enabled' => !empty($modSettings['titlesEnable']),
562
			'input_validate' => function(&$value) use ($smcFunc)
563
			{
564
				if ($smcFunc['strlen']($value) > 50)
565
					return 'user_title_too_long';
566
567
				return true;
568
			},
569
		),
570
		'website_title' => array(
571
			'type' => 'text',
572
			'label' => $txt['website_title'],
573
			'subtext' => $txt['include_website_url'],
574
			'size' => 50,
575
			'permission' => 'profile_website',
576
			'link_with' => 'website',
577
		),
578
		'website_url' => array(
579
			'type' => 'url',
580
			'label' => $txt['website_url'],
581
			'subtext' => $txt['complete_url'],
582
			'size' => 50,
583
			'permission' => 'profile_website',
584
			// Fix the URL...
585
			'input_validate' => function(&$value)
586
			{
587
				if (strlen(trim($value)) > 0 && strpos($value, '://') === false)
588
					$value = 'http://' . $value;
589 View Code Duplication
				if (strlen($value) < 8 || (substr($value, 0, 7) !== 'http://' && substr($value, 0, 8) !== 'https://'))
590
					$value = '';
591
				$value = (string) validate_iri(sanitize_iri($value));
0 ignored issues
show
Bug introduced by
It seems like sanitize_iri($value) targeting sanitize_iri() can also be of type boolean; however, validate_iri() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
592
				return true;
593
			},
594
			'link_with' => 'website',
595
		),
596
	);
597
598
	call_integration_hook('integrate_load_profile_fields', array(&$profile_fields));
599
600
	$disabled_fields = !empty($modSettings['disabled_profile_fields']) ? explode(',', $modSettings['disabled_profile_fields']) : array();
601
	// For each of the above let's take out the bits which don't apply - to save memory and security!
602
	foreach ($profile_fields as $key => $field)
603
	{
604
		// Do we have permission to do this?
605
		if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? array($field['permission'] . '_own', $field['permission'] . '_any') : $field['permission'] . '_any')) && !allowedTo($field['permission']))
606
			unset($profile_fields[$key]);
607
608
		// Is it enabled?
609
		if (isset($field['enabled']) && !$field['enabled'])
610
			unset($profile_fields[$key]);
611
612
		// Is it specifically disabled?
613
		if (in_array($key, $disabled_fields) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields)))
614
			unset($profile_fields[$key]);
615
	}
616
}
617
618
/**
619
 * Setup the context for a page load!
620
 *
621
 * @param array $fields The profile fields to display. Each item should correspond to an item in the $profile_fields array generated by loadProfileFields
622
 */
623
function setupProfileContext($fields)
624
{
625
	global $profile_fields, $context, $cur_profile, $txt;
626
627
	// Some default bits.
628
	$context['profile_prehtml'] = '';
629
	$context['profile_posthtml'] = '';
630
	$context['profile_javascript'] = '';
631
	$context['profile_onsubmit_javascript'] = '';
632
633
	call_integration_hook('integrate_setup_profile_context', array(&$fields));
634
635
	// Make sure we have this!
636
	loadProfileFields(true);
637
638
	// First check for any linked sets.
639
	foreach ($profile_fields as $key => $field)
640
		if (isset($field['link_with']) && in_array($field['link_with'], $fields))
641
			$fields[] = $key;
642
643
	$i = 0;
644
	$last_type = '';
645
	foreach ($fields as $key => $field)
646
	{
647
		if (isset($profile_fields[$field]))
648
		{
649
			// Shortcut.
650
			$cur_field = &$profile_fields[$field];
651
652
			// Does it have a preload and does that preload succeed?
653
			if (isset($cur_field['preload']) && !$cur_field['preload']())
654
				continue;
655
656
			// If this is anything but complex we need to do more cleaning!
657
			if ($cur_field['type'] != 'callback' && $cur_field['type'] != 'hidden')
658
			{
659
				if (!isset($cur_field['label']))
660
					$cur_field['label'] = isset($txt[$field]) ? $txt[$field] : $field;
661
662
				// Everything has a value!
663
				if (!isset($cur_field['value']))
664
					$cur_field['value'] = isset($cur_profile[$field]) ? $cur_profile[$field] : '';
665
666
				// Any input attributes?
667
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
668
			}
669
670
			// Was there an error with this field on posting?
671
			if (isset($context['profile_errors'][$field]))
672
				$cur_field['is_error'] = true;
673
674
			// Any javascript stuff?
675
			if (!empty($cur_field['js_submit']))
676
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
677
			if (!empty($cur_field['js']))
678
				$context['profile_javascript'] .= $cur_field['js'];
679
680
			// Any template stuff?
681
			if (!empty($cur_field['prehtml']))
682
				$context['profile_prehtml'] .= $cur_field['prehtml'];
683
			if (!empty($cur_field['posthtml']))
684
				$context['profile_posthtml'] .= $cur_field['posthtml'];
685
686
			// Finally put it into context?
687
			if ($cur_field['type'] != 'hidden')
688
			{
689
				$last_type = $cur_field['type'];
690
				$context['profile_fields'][$field] = &$profile_fields[$field];
691
			}
692
		}
693
		// Bodge in a line break - without doing two in a row ;)
694
		elseif ($field == 'hr' && $last_type != 'hr' && $last_type != '')
695
		{
696
			$last_type = 'hr';
697
			$context['profile_fields'][$i++]['type'] = 'hr';
698
		}
699
	}
700
701
	// Some spicy JS.
702
	addInlineJavaScript('
703
	var form_handle = document.forms.creator;
704
	createEventListener(form_handle);
705
	'. (!empty($context['require_password']) ? '
706
	form_handle.addEventListener(\'submit\', function(event)
707
	{
708
		if (this.oldpasswrd.value == "")
709
		{
710
			event.preventDefault();
711
			alert('. (JavaScriptEscape($txt['required_security_reasons'])) . ');
712
			return false;
713
		}
714
	}, false);' : ''), true);
715
716
	// Any onsubmit javascript?
717
	if (!empty($context['profile_onsubmit_javascript']))
718
		addInlineJavaScript($context['profile_onsubmit_javascript'], true);
719
720
	// Any totally custom stuff?
721
	if (!empty($context['profile_javascript']))
722
		addInlineJavaScript($context['profile_javascript'], true);
723
724
	// Free up some memory.
725
	unset($profile_fields);
726
}
727
728
/**
729
 * Save the profile changes.
730
 */
731
function saveProfileFields()
732
{
733
	global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile;
734
735
	// Load them up.
736
	loadProfileFields();
737
738
	// This makes things easier...
739
	$old_profile = $cur_profile;
740
741
	// This allows variables to call activities when they save - by default just to reload their settings
742
	$context['profile_execute_on_save'] = array();
743
	if ($context['user']['is_owner'])
744
		$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
745
746
	// Assume we log nothing.
747
	$context['log_changes'] = array();
748
749
	// Cycle through the profile fields working out what to do!
750
	foreach ($profile_fields as $key => $field)
751
	{
752
		if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key == 'signature'))
753
			continue;
754
755
		// What gets updated?
756
		$db_key = isset($field['save_key']) ? $field['save_key'] : $key;
757
758
		// Right - we have something that is enabled, we can act upon and has a value posted to it. Does it have a validation function?
759
		if (isset($field['input_validate']))
760
		{
761
			$is_valid = $field['input_validate']($_POST[$key]);
762
			// An error occurred - set it as such!
763
			if ($is_valid !== true)
764
			{
765
				// Is this an actual error?
766
				if ($is_valid !== false)
767
				{
768
					$post_errors[$key] = $is_valid;
769
					$profile_fields[$key]['is_error'] = $is_valid;
770
				}
771
				// Retain the old value.
772
				$cur_profile[$key] = $_POST[$key];
773
				continue;
774
			}
775
		}
776
777
		// Are we doing a cast?
778
		$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
779
780
		// Finally, clean up certain types.
781
		if ($field['cast_type'] == 'int')
782
			$_POST[$key] = (int) $_POST[$key];
783
		elseif ($field['cast_type'] == 'float')
784
			$_POST[$key] = (float) $_POST[$key];
785
		elseif ($field['cast_type'] == 'check')
786
			$_POST[$key] = !empty($_POST[$key]) ? 1 : 0;
787
788
		// If we got here we're doing OK.
789
		if ($field['type'] != 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
790
		{
791
			// Set the save variable.
792
			$profile_vars[$db_key] = $_POST[$key];
793
			// And update the user profile.
794
			$cur_profile[$key] = $_POST[$key];
795
796
			// Are we logging it?
797
			if (!empty($field['log_change']) && isset($old_profile[$key]))
798
				$context['log_changes'][$key] = array(
799
					'previous' => $old_profile[$key],
800
					'new' => $_POST[$key],
801
				);
802
		}
803
804
		// Logging group changes are a bit different...
805
		if ($key == 'id_group' && $field['log_change'])
806
		{
807
			profileLoadGroups();
808
809
			// Any changes to primary group?
810
			if ($_POST['id_group'] != $old_profile['id_group'])
811
			{
812
				$context['log_changes']['id_group'] = array(
813
					'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
814
					'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
815
				);
816
			}
817
818
			// Prepare additional groups for comparison.
819
			$additional_groups = array(
820
				'previous' => !empty($old_profile['additional_groups']) ? explode(',', $old_profile['additional_groups']) : array(),
821
				'new' => !empty($_POST['additional_groups']) ? array_diff($_POST['additional_groups'], array(0)) : array(),
822
			);
823
824
			sort($additional_groups['previous']);
825
			sort($additional_groups['new']);
826
827
			// What about additional groups?
828
			if ($additional_groups['previous'] != $additional_groups['new'])
829
			{
830
				foreach ($additional_groups as $type => $groups)
831
				{
832
					foreach ($groups as $id => $group)
0 ignored issues
show
Bug introduced by
The expression $groups of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
833
					{
834
						if (isset($context['member_groups'][$group]))
835
							$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
836
						else
837
							unset($additional_groups[$type][$id]);
838
					}
839
					$additional_groups[$type] = implode(', ', $additional_groups[$type]);
840
				}
841
842
				$context['log_changes']['additional_groups'] = $additional_groups;
843
			}
844
		}
845
	}
846
847
	// @todo Temporary
848
	if ($context['user']['is_owner'])
849
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
850
	else
851
		$changeOther = allowedTo('profile_extra_any');
852
	if ($changeOther && empty($post_errors))
853
	{
854
		makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
855
		if (!empty($_REQUEST['sa']))
856
		{
857
			$custom_fields_errors = makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false, true);
858
859
			if (!empty($custom_fields_errors))
860
				$post_errors = array_merge($post_errors, $custom_fields_errors);
861
		}
862
	}
863
864
	// Free memory!
865
	unset($profile_fields);
866
}
867
868
/**
869
 * Save the profile changes
870
 *
871
 * @param array &$profile_vars The items to save
872
 * @param array &$post_errors An array of information about any errors that occurred
873
 * @param int $memID The ID of the member whose profile we're saving
874
 */
875
function saveProfileChanges(&$profile_vars, &$post_errors, $memID)
0 ignored issues
show
Unused Code introduced by
The parameter $post_errors is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
876
{
877
	global $user_profile, $context;
878
879
	// These make life easier....
880
	$old_profile = &$user_profile[$memID];
881
882
	// Permissions...
883
	if ($context['user']['is_owner'])
884
	{
885
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own', 'profile_website_any', 'profile_website_own', 'profile_signature_any', 'profile_signature_own'));
886
	}
887
	else
888
		$changeOther = allowedTo(array('profile_extra_any', 'profile_website_any', 'profile_signature_any'));
889
890
	// Arrays of all the changes - makes things easier.
891
	$profile_bools = array();
892
	$profile_ints = array();
893
	$profile_floats = array();
894
	$profile_strings = array(
895
		'buddy_list',
896
		'ignore_boards',
897
	);
898
899
	if (isset($_POST['sa']) && $_POST['sa'] == 'ignoreboards' && empty($_POST['ignore_brd']))
900
		$_POST['ignore_brd'] = array();
901
902
	unset($_POST['ignore_boards']); // Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
903
	if (isset($_POST['ignore_brd']))
904
	{
905
		if (!is_array($_POST['ignore_brd']))
906
			$_POST['ignore_brd'] = array($_POST['ignore_brd']);
907
908
		foreach ($_POST['ignore_brd'] as $k => $d)
909
		{
910
			$d = (int) $d;
911
			if ($d != 0)
912
				$_POST['ignore_brd'][$k] = $d;
913
			else
914
				unset($_POST['ignore_brd'][$k]);
915
		}
916
		$_POST['ignore_boards'] = implode(',', $_POST['ignore_brd']);
917
		unset($_POST['ignore_brd']);
918
	}
919
920
	// Here's where we sort out all the 'other' values...
921
	if ($changeOther)
922
	{
923
		makeThemeChanges($memID, isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
924
		//makeAvatarChanges($memID, $post_errors);
925
926
		if (!empty($_REQUEST['sa']))
927
			makeCustomFieldChanges($memID, $_REQUEST['sa'], false);
928
929
		foreach ($profile_bools as $var)
930
			if (isset($_POST[$var]))
931
				$profile_vars[$var] = empty($_POST[$var]) ? '0' : '1';
932
		foreach ($profile_ints as $var)
933
			if (isset($_POST[$var]))
934
				$profile_vars[$var] = $_POST[$var] != '' ? (int) $_POST[$var] : '';
935
		foreach ($profile_floats as $var)
936
			if (isset($_POST[$var]))
937
				$profile_vars[$var] = (float) $_POST[$var];
938
		foreach ($profile_strings as $var)
939
			if (isset($_POST[$var]))
940
				$profile_vars[$var] = $_POST[$var];
941
	}
942
}
943
944
/**
945
 * Make any theme changes that are sent with the profile.
946
 *
947
 * @param int $memID The ID of the user
948
 * @param int $id_theme The ID of the theme
949
 */
950
function makeThemeChanges($memID, $id_theme)
951
{
952
	global $modSettings, $smcFunc, $context, $user_info;
953
954
	$reservedVars = array(
955
		'actual_theme_url',
956
		'actual_images_url',
957
		'base_theme_dir',
958
		'base_theme_url',
959
		'default_images_url',
960
		'default_theme_dir',
961
		'default_theme_url',
962
		'default_template',
963
		'images_url',
964
		'number_recent_posts',
965
		'smiley_sets_default',
966
		'theme_dir',
967
		'theme_id',
968
		'theme_layers',
969
		'theme_templates',
970
		'theme_url',
971
	);
972
973
	// Can't change reserved vars.
974
	if ((isset($_POST['options']) && count(array_intersect(array_keys($_POST['options']), $reservedVars)) != 0) || (isset($_POST['default_options']) && count(array_intersect(array_keys($_POST['default_options']), $reservedVars)) != 0))
975
		fatal_lang_error('no_access', false);
976
977
	// Don't allow any overriding of custom fields with default or non-default options.
978
	$request = $smcFunc['db_query']('', '
979
		SELECT col_name
980
		FROM {db_prefix}custom_fields
981
		WHERE active = {int:is_active}',
982
		array(
983
			'is_active' => 1,
984
		)
985
	);
986
	$custom_fields = array();
987
	while ($row = $smcFunc['db_fetch_assoc']($request))
988
		$custom_fields[] = $row['col_name'];
989
	$smcFunc['db_free_result']($request);
990
991
	// These are the theme changes...
992
	$themeSetArray = array();
993
	if (isset($_POST['options']) && is_array($_POST['options']))
994
	{
995
		foreach ($_POST['options'] as $opt => $val)
996
		{
997
			if (in_array($opt, $custom_fields))
998
				continue;
999
1000
			// These need to be controlled.
1001
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1002
				$val = max(0, min($val, 50));
1003
			// We don't set this per theme anymore.
1004
			elseif ($opt == 'allow_no_censored')
1005
				continue;
1006
1007
			$themeSetArray[] = array($memID, $id_theme, $opt, is_array($val) ? implode(',', $val) : $val);
1008
		}
1009
	}
1010
1011
	$erase_options = array();
1012
	if (isset($_POST['default_options']) && is_array($_POST['default_options']))
1013
		foreach ($_POST['default_options'] as $opt => $val)
1014
		{
1015
			if (in_array($opt, $custom_fields))
1016
				continue;
1017
1018
			// These need to be controlled.
1019
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1020
				$val = max(0, min($val, 50));
1021
			// Only let admins and owners change the censor.
1022
			elseif ($opt == 'allow_no_censored' && !$user_info['is_admin'] && !$context['user']['is_owner'])
1023
					continue;
1024
1025
			$themeSetArray[] = array($memID, 1, $opt, is_array($val) ? implode(',', $val) : $val);
1026
			$erase_options[] = $opt;
1027
		}
1028
1029
	// If themeSetArray isn't still empty, send it to the database.
1030
	if (empty($context['password_auth_failed']))
1031
	{
1032 View Code Duplication
		if (!empty($themeSetArray))
1033
		{
1034
			$smcFunc['db_insert']('replace',
1035
				'{db_prefix}themes',
1036
				array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
1037
				$themeSetArray,
1038
				array('id_member', 'id_theme', 'variable')
1039
			);
1040
		}
1041
1042
		if (!empty($erase_options))
1043
		{
1044
			$smcFunc['db_query']('', '
1045
				DELETE FROM {db_prefix}themes
1046
				WHERE id_theme != {int:id_theme}
1047
					AND variable IN ({array_string:erase_variables})
1048
					AND id_member = {int:id_member}',
1049
				array(
1050
					'id_theme' => 1,
1051
					'id_member' => $memID,
1052
					'erase_variables' => $erase_options
1053
				)
1054
			);
1055
		}
1056
1057
		// Admins can choose any theme, even if it's not enabled...
1058
		$themes = allowedTo('admin_forum') ? explode(',', $modSettings['knownThemes']) : explode(',', $modSettings['enableThemes']);
1059
		foreach ($themes as $t)
1060
			cache_put_data('theme_settings-' . $t . ':' . $memID, null, 60);
1061
	}
1062
}
1063
1064
/**
1065
 * Make any notification changes that need to be made.
1066
 *
1067
 * @param int $memID The ID of the member
1068
 */
1069
function makeNotificationChanges($memID)
1070
{
1071
	global $smcFunc, $sourcedir;
1072
1073
	require_once($sourcedir . '/Subs-Notify.php');
1074
1075
	// Update the boards they are being notified on.
1076
	if (isset($_POST['edit_notify_boards']) && !empty($_POST['notify_boards']))
1077
	{
1078
		// Make sure only integers are deleted.
1079
		foreach ($_POST['notify_boards'] as $index => $id)
1080
			$_POST['notify_boards'][$index] = (int) $id;
1081
1082
		// id_board = 0 is reserved for topic notifications.
1083
		$_POST['notify_boards'] = array_diff($_POST['notify_boards'], array(0));
1084
1085
		$smcFunc['db_query']('', '
1086
			DELETE FROM {db_prefix}log_notify
1087
			WHERE id_board IN ({array_int:board_list})
1088
				AND id_member = {int:selected_member}',
1089
			array(
1090
				'board_list' => $_POST['notify_boards'],
1091
				'selected_member' => $memID,
1092
			)
1093
		);
1094
	}
1095
1096
	// We are editing topic notifications......
1097
	elseif (isset($_POST['edit_notify_topics']) && !empty($_POST['notify_topics']))
1098
	{
1099
		foreach ($_POST['notify_topics'] as $index => $id)
1100
			$_POST['notify_topics'][$index] = (int) $id;
1101
1102
		// Make sure there are no zeros left.
1103
		$_POST['notify_topics'] = array_diff($_POST['notify_topics'], array(0));
1104
1105
		$smcFunc['db_query']('', '
1106
			DELETE FROM {db_prefix}log_notify
1107
			WHERE id_topic IN ({array_int:topic_list})
1108
				AND id_member = {int:selected_member}',
1109
			array(
1110
				'topic_list' => $_POST['notify_topics'],
1111
				'selected_member' => $memID,
1112
			)
1113
		);
1114
		foreach ($_POST['notify_topics'] as $topic)
1115
			setNotifyPrefs($memID, array('topic_notify_' . $topic => 0));
1116
	}
1117
1118
	// We are removing topic preferences
1119
	elseif (isset($_POST['remove_notify_topics']) && !empty($_POST['notify_topics']))
1120
	{
1121
		$prefs = array();
1122
		foreach ($_POST['notify_topics'] as $topic)
1123
			$prefs[] = 'topic_notify_' . $topic;
1124
		deleteNotifyPrefs($memID, $prefs);
1125
	}
1126
1127
	// We are removing board preferences
1128
	elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
1129
	{
1130
		$prefs = array();
1131
		foreach ($_POST['notify_boards'] as $board)
1132
			$prefs[] = 'board_notify_' . $board;
1133
		deleteNotifyPrefs($memID, $prefs);
1134
	}
1135
}
1136
1137
/**
1138
 * Save any changes to the custom profile fields
1139
 *
1140
 * @param int $memID The ID of the member
1141
 * @param string $area The area of the profile these fields are in
1142
 * @param bool $sanitize = true Whether or not to sanitize the data
1143
 * @param bool $returnErrors Whether or not to return any error information
1144
 * @return void|array Returns nothing or returns an array of error info if $returnErrors is true
1145
 */
1146
function makeCustomFieldChanges($memID, $area, $sanitize = true, $returnErrors = false)
1147
{
1148
	global $context, $smcFunc, $user_profile, $user_info, $modSettings;
1149
	global $sourcedir;
1150
1151
	$errors = array();
1152
1153
	if ($sanitize && isset($_POST['customfield']))
1154
		$_POST['customfield'] = htmlspecialchars__recursive($_POST['customfield']);
1155
1156
	$where = $area == 'register' ? 'show_reg != 0' : 'show_profile = {string:area}';
1157
1158
	// Load the fields we are saving too - make sure we save valid data (etc).
1159
	$request = $smcFunc['db_query']('', '
1160
		SELECT col_name, field_name, field_desc, field_type, field_length, field_options, default_value, show_reg, mask, private
1161
		FROM {db_prefix}custom_fields
1162
		WHERE ' . $where . '
1163
			AND active = {int:is_active}',
1164
		array(
1165
			'is_active' => 1,
1166
			'area' => $area,
1167
		)
1168
	);
1169
	$changes = array();
1170
	$deletes = array();
1171
	$log_changes = array();
1172
	while ($row = $smcFunc['db_fetch_assoc']($request))
1173
	{
1174
		/* This means don't save if:
1175
			- The user is NOT an admin.
1176
			- The data is not freely viewable and editable by users.
1177
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
1178
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
1179
		*/
1180
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != $user_info['id'] || $row['private'] != 2) && ($area != 'register' || $row['show_reg'] == 0))
1181
			continue;
1182
1183
		// Validate the user data.
1184
		if ($row['field_type'] == 'check')
1185
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
1186
		elseif ($row['field_type'] == 'select' || $row['field_type'] == 'radio')
1187
		{
1188
			$value = $row['default_value'];
1189
			foreach (explode(',', $row['field_options']) as $k => $v)
1190
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
1191
					$value = $v;
1192
		}
1193
		// Otherwise some form of text!
1194
		else
1195
		{
1196
			$value = isset($_POST['customfield'][$row['col_name']]) ? $_POST['customfield'][$row['col_name']] : '';
1197
1198
			if ($row['field_length'])
1199
				$value = $smcFunc['substr']($value, 0, $row['field_length']);
1200
1201
			// Any masks?
1202
			if ($row['field_type'] == 'text' && !empty($row['mask']) && $row['mask'] != 'none')
1203
			{
1204
				$value = $smcFunc['htmltrim']($value);
1205
				$valueReference = un_htmlspecialchars($value);
1206
1207
				// Try and avoid some checks. '0' could be a valid non-empty value.
1208
				if (empty($value) && !is_numeric($value))
1209
					$value = '';
1210
1211
				if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $value != filter_var($value, FILTER_SANITIZE_STRING) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
1212
				{
1213
					if ($returnErrors)
1214
						$errors[] = 'custom_field_nohtml_fail';
1215
1216
					else
1217
						$value = '';
1218
				}
1219
				elseif ($row['mask'] == 'email' && (!filter_var($value, FILTER_VALIDATE_EMAIL) || strlen($value) > 255))
1220
				{
1221
					if ($returnErrors)
1222
						$errors[] = 'custom_field_mail_fail';
1223
1224
					else
1225
						$value = '';
1226
				}
1227
				elseif ($row['mask'] == 'number')
1228
				{
1229
					$value = (int) $value;
1230
				}
1231
				elseif (substr($row['mask'], 0, 5) == 'regex' && trim($value) != '' && preg_match(substr($row['mask'], 5), $value) === 0)
1232
				{
1233
					if ($returnErrors)
1234
						$errors[] = 'custom_field_regex_fail';
1235
1236
					else
1237
						$value = '';
1238
				}
1239
1240
				unset($valueReference);
1241
			}
1242
		}
1243
1244
		// Did it change?
1245
		if (!isset($user_profile[$memID]['options'][$row['col_name']]) || $user_profile[$memID]['options'][$row['col_name']] !== $value)
1246
		{
1247
			$log_changes[] = array(
1248
				'action' => 'customfield_' . $row['col_name'],
1249
				'log_type' => 'user',
1250
				'extra' => array(
1251
					'previous' => !empty($user_profile[$memID]['options'][$row['col_name']]) ? $user_profile[$memID]['options'][$row['col_name']] : '',
1252
					'new' => $value,
1253
					'applicator' => $user_info['id'],
1254
					'member_affected' => $memID,
1255
				),
1256
			);
1257
			if (empty($value))
1258
			{
1259
				$deletes = array('id_theme' => 1 , 'variable' => $row['col_name'], 'id_member' => $memID);
1260
				unset($user_profile[$memID]['options'][$row['col_name']]);
1261
			}
1262
			else
1263
			{
1264
				$changes[] = array(1, $row['col_name'], $value, $memID);
1265
				$user_profile[$memID]['options'][$row['col_name']] = $value;
1266
			}
1267
		}
1268
	}
1269
	$smcFunc['db_free_result']($request);
1270
1271
	$hook_errors = call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, &$errors, $returnErrors, $memID, $area, $sanitize, &$deletes));
1272
1273
	if (!empty($hook_errors) && is_array($hook_errors))
1274
		$errors = array_merge($errors, $hook_errors);
1275
1276
	// Make those changes!
1277
	if ((!empty($changes) || !empty($deletes)) && empty($context['password_auth_failed']) && empty($errors))
1278
	{
1279 View Code Duplication
		if (!empty($changes))
1280
			$smcFunc['db_insert']('replace',
1281
				'{db_prefix}themes',
1282
				array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
1283
				$changes,
1284
				array('id_theme', 'variable', 'id_member')
1285
			);
1286
		if (!empty($deletes))
1287
			$smcFunc['db_query']('','
1288
				DELETE FROM {db_prefix}themes
1289
				WHERE id_theme = {int:id_theme} AND
1290
						variable = {string:variable} AND
1291
						id_member = {int:id_member}',
1292
				$deletes
1293
				);
1294
		if (!empty($log_changes) && !empty($modSettings['modlog_enabled']))
1295
		{
1296
			require_once($sourcedir . '/Logging.php');
1297
			logActions($log_changes);
1298
		}
1299
	}
1300
1301
	if ($returnErrors)
1302
		return $errors;
1303
}
1304
1305
/**
1306
 * Show all the users buddies, as well as a add/delete interface.
1307
 *
1308
 * @param int $memID The ID of the member
1309
 */
1310
function editBuddyIgnoreLists($memID)
1311
{
1312
	global $context, $txt, $modSettings;
1313
1314
	// Do a quick check to ensure people aren't getting here illegally!
1315
	if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
1316
		fatal_lang_error('no_access', false);
1317
1318
	// Can we email the user direct?
1319
	$context['can_moderate_forum'] = allowedTo('moderate_forum');
1320
	$context['can_send_email'] = allowedTo('moderate_forum');
1321
1322
	$subActions = array(
1323
		'buddies' => array('editBuddies', $txt['editBuddies']),
1324
		'ignore' => array('editIgnoreList', $txt['editIgnoreList']),
1325
	);
1326
1327
	$context['list_area'] = isset($_GET['sa']) && isset($subActions[$_GET['sa']]) ? $_GET['sa'] : 'buddies';
1328
1329
	// Create the tabs for the template.
1330
	$context[$context['profile_menu_name']]['tab_data'] = array(
1331
		'title' => $txt['editBuddyIgnoreLists'],
1332
		'description' => $txt['buddy_ignore_desc'],
1333
		'icon' => 'profile_hd.png',
1334
		'tabs' => array(
1335
			'buddies' => array(),
1336
			'ignore' => array(),
1337
		),
1338
	);
1339
1340
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
1341
1342
	// Pass on to the actual function.
1343
	$context['sub_template'] = $subActions[$context['list_area']][0];
1344
	$call = call_helper($subActions[$context['list_area']][0], true);
1345
1346
	if (!empty($call))
1347
		call_user_func($call, $memID);
1348
}
1349
1350
/**
1351
 * Show all the users buddies, as well as a add/delete interface.
1352
 *
1353
 * @param int $memID The ID of the member
1354
 */
1355
function editBuddies($memID)
1356
{
1357
	global $txt, $scripturl, $settings;
1358
	global $context, $user_profile, $memberContext, $smcFunc;
1359
1360
	// For making changes!
1361
	$buddiesArray = explode(',', $user_profile[$memID]['buddy_list']);
1362
	foreach ($buddiesArray as $k => $dummy)
1363
		if ($dummy == '')
1364
			unset($buddiesArray[$k]);
1365
1366
	// Removing a buddy?
1367
	if (isset($_GET['remove']))
1368
	{
1369
		checkSession('get');
1370
1371
		call_integration_hook('integrate_remove_buddy', array($memID));
1372
1373
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1374
1375
		// Heh, I'm lazy, do it the easy way...
1376
		foreach ($buddiesArray as $key => $buddy)
1377
			if ($buddy == (int) $_GET['remove'])
1378
			{
1379
				unset($buddiesArray[$key]);
1380
				$_SESSION['prf-save'] = true;
1381
			}
1382
1383
		// Make the changes.
1384
		$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1385
		updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1386
1387
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1388
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1389
	}
1390
	elseif (isset($_POST['new_buddy']))
1391
	{
1392
		checkSession();
1393
1394
		// Prepare the string for extraction...
1395
		$_POST['new_buddy'] = strtr($smcFunc['htmlspecialchars']($_POST['new_buddy'], ENT_QUOTES), array('&quot;' => '"'));
1396
		preg_match_all('~"([^"]+)"~', $_POST['new_buddy'], $matches);
1397
		$new_buddies = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_buddy']))));
1398
1399 View Code Duplication
		foreach ($new_buddies as $k => $dummy)
1400
		{
1401
			$new_buddies[$k] = strtr(trim($new_buddies[$k]), array('\'' => '&#039;'));
1402
1403
			if (strlen($new_buddies[$k]) == 0 || in_array($new_buddies[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1404
				unset($new_buddies[$k]);
1405
		}
1406
1407
		call_integration_hook('integrate_add_buddies', array($memID, &$new_buddies));
1408
1409
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1410 View Code Duplication
		if (!empty($new_buddies))
1411
		{
1412
			// Now find out the id_member of the buddy.
1413
			$request = $smcFunc['db_query']('', '
1414
				SELECT id_member
1415
				FROM {db_prefix}members
1416
				WHERE member_name IN ({array_string:new_buddies}) OR real_name IN ({array_string:new_buddies})
1417
				LIMIT {int:count_new_buddies}',
1418
				array(
1419
					'new_buddies' => $new_buddies,
1420
					'count_new_buddies' => count($new_buddies),
1421
				)
1422
			);
1423
1424
			if ($smcFunc['db_num_rows']($request) != 0)
1425
				$_SESSION['prf-save'] = true;
1426
1427
			// Add the new member to the buddies array.
1428
			while ($row = $smcFunc['db_fetch_assoc']($request))
1429
			{
1430
				if (in_array($row['id_member'], $buddiesArray))
1431
					continue;
1432
				else
1433
					$buddiesArray[] = (int) $row['id_member'];
1434
			}
1435
			$smcFunc['db_free_result']($request);
1436
1437
			// Now update the current users buddy list.
1438
			$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1439
			updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1440
		}
1441
1442
		// Back to the buddy list!
1443
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1444
	}
1445
1446
	// Get all the users "buddies"...
1447
	$buddies = array();
1448
1449
	// Gotta load the custom profile fields names.
1450
	$request = $smcFunc['db_query']('', '
1451
		SELECT col_name, field_name, field_desc, field_type, bbc, enclose
1452
		FROM {db_prefix}custom_fields
1453
		WHERE active = {int:active}
1454
			AND private < {int:private_level}',
1455
		array(
1456
			'active' => 1,
1457
			'private_level' => 2,
1458
		)
1459
	);
1460
1461
	$context['custom_pf'] = array();
1462
	$disabled_fields = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
0 ignored issues
show
Bug introduced by
The variable $modSettings does not exist. Did you mean $settings?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1463
	while ($row = $smcFunc['db_fetch_assoc']($request))
1464
		if (!isset($disabled_fields[$row['col_name']]))
1465
			$context['custom_pf'][$row['col_name']] = array(
1466
				'label' => $row['field_name'],
1467
				'type' => $row['field_type'],
1468
				'bbc' => !empty($row['bbc']),
1469
				'enclose' => $row['enclose'],
1470
			);
1471
1472
	// Gotta disable the gender option.
1473
	if (isset($context['custom_pf']['cust_gender']) && $context['custom_pf']['cust_gender'] == 'None')
1474
		unset($context['custom_pf']['cust_gender']);
1475
1476
	$smcFunc['db_free_result']($request);
1477
1478 View Code Duplication
	if (!empty($buddiesArray))
1479
	{
1480
		$result = $smcFunc['db_query']('', '
1481
			SELECT id_member
1482
			FROM {db_prefix}members
1483
			WHERE id_member IN ({array_int:buddy_list})
1484
			ORDER BY real_name
1485
			LIMIT {int:buddy_list_count}',
1486
			array(
1487
				'buddy_list' => $buddiesArray,
1488
				'buddy_list_count' => substr_count($user_profile[$memID]['buddy_list'], ',') + 1,
1489
			)
1490
		);
1491
		while ($row = $smcFunc['db_fetch_assoc']($result))
1492
			$buddies[] = $row['id_member'];
1493
		$smcFunc['db_free_result']($result);
1494
	}
1495
1496
	$context['buddy_count'] = count($buddies);
1497
1498
	// Load all the members up.
1499
	loadMemberData($buddies, false, 'profile');
1500
1501
	// Setup the context for each buddy.
1502
	$context['buddies'] = array();
1503
	foreach ($buddies as $buddy)
1504
	{
1505
		loadMemberContext($buddy);
1506
		$context['buddies'][$buddy] = $memberContext[$buddy];
1507
1508
		// Make sure to load the appropriate fields for each user
1509
		if (!empty($context['custom_pf']))
1510
		{
1511
			foreach ($context['custom_pf'] as $key => $column)
0 ignored issues
show
Bug introduced by
The expression $context['custom_pf'] of type array|integer is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1512
			{
1513
				// Don't show anything if there isn't anything to show.
1514 View Code Duplication
				if (!isset($context['buddies'][$buddy]['options'][$key]))
1515
				{
1516
					$context['buddies'][$buddy]['options'][$key] = '';
1517
					continue;
1518
				}
1519
1520 View Code Duplication
				if ($column['bbc'] && !empty($context['buddies'][$buddy]['options'][$key]))
1521
					$context['buddies'][$buddy]['options'][$key] = strip_tags(parse_bbc($context['buddies'][$buddy]['options'][$key]));
1522
1523
				elseif ($column['type'] == 'check')
1524
					$context['buddies'][$buddy]['options'][$key] = $context['buddies'][$buddy]['options'][$key] == 0 ? $txt['no'] : $txt['yes'];
1525
1526
				// Enclosing the user input within some other text?
1527
				if (!empty($column['enclose']) && !empty($context['buddies'][$buddy]['options'][$key]))
1528
					$context['buddies'][$buddy]['options'][$key] = strtr($column['enclose'], array(
1529
						'{SCRIPTURL}' => $scripturl,
1530
						'{IMAGES_URL}' => $settings['images_url'],
1531
						'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1532
						'{INPUT}' => $context['buddies'][$buddy]['options'][$key],
1533
					));
1534
			}
1535
		}
1536
	}
1537
1538 View Code Duplication
	if (isset($_SESSION['prf-save']))
1539
	{
1540
		if ($_SESSION['prf-save'] === true)
1541
			$context['saved_successful'] = true;
1542
		else
1543
			$context['saved_failed'] = $_SESSION['prf-save'];
1544
1545
		unset($_SESSION['prf-save']);
1546
	}
1547
1548
	call_integration_hook('integrate_view_buddies', array($memID));
1549
}
1550
1551
/**
1552
 * Allows the user to view their ignore list, as well as the option to manage members on it.
1553
 *
1554
 * @param int $memID The ID of the member
1555
 */
1556
function editIgnoreList($memID)
1557
{
1558
	global $txt;
1559
	global $context, $user_profile, $memberContext, $smcFunc;
1560
1561
	// For making changes!
1562
	$ignoreArray = explode(',', $user_profile[$memID]['pm_ignore_list']);
1563
	foreach ($ignoreArray as $k => $dummy)
1564
		if ($dummy == '')
1565
			unset($ignoreArray[$k]);
1566
1567
	// Removing a member from the ignore list?
1568
	if (isset($_GET['remove']))
1569
	{
1570
		checkSession('get');
1571
1572
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1573
1574
		// Heh, I'm lazy, do it the easy way...
1575
		foreach ($ignoreArray as $key => $id_remove)
1576
			if ($id_remove == (int) $_GET['remove'])
1577
			{
1578
				unset($ignoreArray[$key]);
1579
				$_SESSION['prf-save'] = true;
1580
			}
1581
1582
		// Make the changes.
1583
		$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1584
		updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1585
1586
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1587
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1588
	}
1589
	elseif (isset($_POST['new_ignore']))
1590
	{
1591
		checkSession();
1592
		// Prepare the string for extraction...
1593
		$_POST['new_ignore'] = strtr($smcFunc['htmlspecialchars']($_POST['new_ignore'], ENT_QUOTES), array('&quot;' => '"'));
1594
		preg_match_all('~"([^"]+)"~', $_POST['new_ignore'], $matches);
1595
		$new_entries = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_ignore']))));
1596
1597 View Code Duplication
		foreach ($new_entries as $k => $dummy)
1598
		{
1599
			$new_entries[$k] = strtr(trim($new_entries[$k]), array('\'' => '&#039;'));
1600
1601
			if (strlen($new_entries[$k]) == 0 || in_array($new_entries[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1602
				unset($new_entries[$k]);
1603
		}
1604
1605
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1606 View Code Duplication
		if (!empty($new_entries))
1607
		{
1608
			// Now find out the id_member for the members in question.
1609
			$request = $smcFunc['db_query']('', '
1610
				SELECT id_member
1611
				FROM {db_prefix}members
1612
				WHERE member_name IN ({array_string:new_entries}) OR real_name IN ({array_string:new_entries})
1613
				LIMIT {int:count_new_entries}',
1614
				array(
1615
					'new_entries' => $new_entries,
1616
					'count_new_entries' => count($new_entries),
1617
				)
1618
			);
1619
1620
			if ($smcFunc['db_num_rows']($request) != 0)
1621
				$_SESSION['prf-save'] = true;
1622
1623
			// Add the new member to the buddies array.
1624
			while ($row = $smcFunc['db_fetch_assoc']($request))
1625
			{
1626
				if (in_array($row['id_member'], $ignoreArray))
1627
					continue;
1628
				else
1629
					$ignoreArray[] = (int) $row['id_member'];
1630
			}
1631
			$smcFunc['db_free_result']($request);
1632
1633
			// Now update the current users buddy list.
1634
			$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1635
			updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1636
		}
1637
1638
		// Back to the list of pityful people!
1639
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1640
	}
1641
1642
	// Initialise the list of members we're ignoring.
1643
	$ignored = array();
1644
1645 View Code Duplication
	if (!empty($ignoreArray))
1646
	{
1647
		$result = $smcFunc['db_query']('', '
1648
			SELECT id_member
1649
			FROM {db_prefix}members
1650
			WHERE id_member IN ({array_int:ignore_list})
1651
			ORDER BY real_name
1652
			LIMIT {int:ignore_list_count}',
1653
			array(
1654
				'ignore_list' => $ignoreArray,
1655
				'ignore_list_count' => substr_count($user_profile[$memID]['pm_ignore_list'], ',') + 1,
1656
			)
1657
		);
1658
		while ($row = $smcFunc['db_fetch_assoc']($result))
1659
			$ignored[] = $row['id_member'];
1660
		$smcFunc['db_free_result']($result);
1661
	}
1662
1663
	$context['ignore_count'] = count($ignored);
1664
1665
	// Load all the members up.
1666
	loadMemberData($ignored, false, 'profile');
1667
1668
	// Setup the context for each buddy.
1669
	$context['ignore_list'] = array();
1670
	foreach ($ignored as $ignore_member)
1671
	{
1672
		loadMemberContext($ignore_member);
1673
		$context['ignore_list'][$ignore_member] = $memberContext[$ignore_member];
1674
	}
1675
1676 View Code Duplication
	if (isset($_SESSION['prf-save']))
1677
	{
1678
		if ($_SESSION['prf-save'] === true)
1679
			$context['saved_successful'] = true;
1680
		else
1681
			$context['saved_failed'] = $_SESSION['prf-save'];
1682
1683
		unset($_SESSION['prf-save']);
1684
	}
1685
}
1686
1687
/**
1688
 * Handles the account section of the profile
1689
 *
1690
 * @param int $memID The ID of the member
1691
 */
1692
function account($memID)
1693
{
1694
	global $context, $txt;
1695
1696
	loadThemeOptions($memID);
1697
	if (allowedTo(array('profile_identity_own', 'profile_identity_any', 'profile_password_own', 'profile_password_any')))
1698
		loadCustomFields($memID, 'account');
1699
1700
	$context['sub_template'] = 'edit_options';
1701
	$context['page_desc'] = $txt['account_info'];
1702
1703
	setupProfileContext(
1704
		array(
1705
			'member_name', 'real_name', 'date_registered', 'posts', 'lngfile', 'hr',
1706
			'id_group', 'hr',
1707
			'email_address', 'show_online', 'hr',
1708
			'tfa', 'hr',
1709
			'passwrd1', 'passwrd2', 'hr',
1710
			'secret_question', 'secret_answer',
1711
		)
1712
	);
1713
}
1714
1715
/**
1716
 * Handles the main "Forum Profile" section of the profile
1717
 *
1718
 * @param int $memID The ID of the member
1719
 */
1720
function forumProfile($memID)
1721
{
1722
	global $context, $txt;
1723
1724
	loadThemeOptions($memID);
1725
	if (allowedTo(array('profile_forum_own', 'profile_forum_any')))
1726
		loadCustomFields($memID, 'forumprofile');
1727
1728
	$context['sub_template'] = 'edit_options';
1729
	$context['page_desc'] = $txt['forumProfile_info'];
1730
	$context['show_preview_button'] = true;
1731
1732
	setupProfileContext(
1733
		array(
1734
			'avatar_choice', 'hr', 'personal_text', 'hr',
1735
			'bday1', 'usertitle', 'signature', 'hr',
1736
			'website_title', 'website_url',
1737
		)
1738
	);
1739
}
1740
1741
/**
1742
 * Recursive function to retrieve server-stored avatar files
1743
 *
1744
 * @param string $directory The directory to look for files in
1745
 * @param int $level How many levels we should go in the directory
1746
 * @return array An array of information about the files and directories found
1747
 */
1748
function getAvatars($directory, $level)
1749
{
1750
	global $context, $txt, $modSettings, $smcFunc;
1751
1752
	$result = array();
1753
1754
	// Open the directory..
1755
	$dir = dir($modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory);
1756
	$dirs = array();
1757
	$files = array();
1758
1759
	if (!$dir)
1760
		return array();
1761
1762
	while ($line = $dir->read())
1763
	{
1764
		if (in_array($line, array('.', '..', 'blank.png', 'index.php')))
1765
			continue;
1766
1767
		if (is_dir($modSettings['avatar_directory'] . '/' . $directory . (!empty($directory) ? '/' : '') . $line))
1768
			$dirs[] = $line;
1769
		else
1770
			$files[] = $line;
1771
	}
1772
	$dir->close();
1773
1774
	// Sort the results...
1775
	natcasesort($dirs);
1776
	natcasesort($files);
1777
1778
	if ($level == 0)
1779
	{
1780
		$result[] = array(
1781
			'filename' => 'blank.png',
1782
			'checked' => in_array($context['member']['avatar']['server_pic'], array('', 'blank.png')),
1783
			'name' => $txt['no_pic'],
1784
			'is_dir' => false
1785
		);
1786
	}
1787
1788
	foreach ($dirs as $line)
1789
	{
1790
		$tmp = getAvatars($directory . (!empty($directory) ? '/' : '') . $line, $level + 1);
1791
		if (!empty($tmp))
1792
			$result[] = array(
1793
				'filename' => $smcFunc['htmlspecialchars']($line),
1794
				'checked' => strpos($context['member']['avatar']['server_pic'], $line . '/') !== false,
1795
				'name' => '[' . $smcFunc['htmlspecialchars'](str_replace('_', ' ', $line)) . ']',
1796
				'is_dir' => true,
1797
				'files' => $tmp
1798
		);
1799
		unset($tmp);
1800
	}
1801
1802
	foreach ($files as $line)
1803
	{
1804
		$filename = substr($line, 0, (strlen($line) - strlen(strrchr($line, '.'))));
1805
		$extension = substr(strrchr($line, '.'), 1);
1806
1807
		// Make sure it is an image.
1808
		if (strcasecmp($extension, 'gif') != 0 && strcasecmp($extension, 'jpg') != 0 && strcasecmp($extension, 'jpeg') != 0 && strcasecmp($extension, 'png') != 0 && strcasecmp($extension, 'bmp') != 0)
1809
			continue;
1810
1811
		$result[] = array(
1812
			'filename' => $smcFunc['htmlspecialchars']($line),
1813
			'checked' => $line == $context['member']['avatar']['server_pic'],
1814
			'name' => $smcFunc['htmlspecialchars'](str_replace('_', ' ', $filename)),
1815
			'is_dir' => false
1816
		);
1817
		if ($level == 1)
1818
			$context['avatar_list'][] = $directory . '/' . $line;
1819
	}
1820
1821
	return $result;
1822
}
1823
1824
/**
1825
 * Handles the "Look and Layout" section of the profile
1826
 *
1827
 * @param int $memID The ID of the member
1828
 */
1829
function theme($memID)
1830
{
1831
	global $txt, $context;
1832
1833
	loadTemplate('Settings');
1834
	loadSubTemplate('options');
1835
1836
	// Let mods hook into the theme options.
1837
	call_integration_hook('integrate_theme_options');
1838
1839
	loadThemeOptions($memID);
1840
	if (allowedTo(array('profile_extra_own', 'profile_extra_any')))
1841
		loadCustomFields($memID, 'theme');
1842
1843
	$context['sub_template'] = 'edit_options';
1844
	$context['page_desc'] = $txt['theme_info'];
1845
1846
	setupProfileContext(
1847
		array(
1848
			'id_theme', 'smiley_set', 'hr',
1849
			'time_format', 'timezone', 'hr',
1850
			'theme_settings',
1851
		)
1852
	);
1853
}
1854
1855
/**
1856
 * Display the notifications and settings for changes.
1857
 *
1858
 * @param int $memID The ID of the member
1859
 */
1860
function notification($memID)
1861
{
1862
	global $txt, $context;
1863
1864
	// Going to want this for consistency.
1865
	loadCSSFile('admin.css', array(), 'smf_admin');
1866
1867
	// This is just a bootstrap for everything else.
1868
	$sa = array(
1869
		'alerts' => 'alert_configuration',
1870
		'markread' => 'alert_markread',
1871
		'topics' => 'alert_notifications_topics',
1872
		'boards' => 'alert_notifications_boards',
1873
	);
1874
1875
	$subAction = !empty($_GET['sa']) && isset($sa[$_GET['sa']]) ? $_GET['sa'] : 'alerts';
1876
1877
	$context['sub_template'] = $sa[$subAction];
1878
	$context[$context['profile_menu_name']]['tab_data'] = array(
1879
		'title' => $txt['notification'],
1880
		'help' => '',
1881
		'description' => $txt['notification_info'],
1882
	);
1883
	$sa[$subAction]($memID);
1884
}
1885
1886
/**
1887
 * Handles configuration of alert preferences
1888
 *
1889
 * @param int $memID The ID of the member
1890
 */
1891
function alert_configuration($memID)
1892
{
1893
	global $txt, $context, $modSettings, $smcFunc, $sourcedir;
1894
1895
	if (!isset($context['token_check']))
1896
		$context['token_check'] = 'profile-nt' . $memID;
1897
1898
	is_not_guest();
1899
	if (!$context['user']['is_owner'])
1900
		isAllowedTo('profile_extra_any');
1901
1902
	// Set the post action if we're coming from the profile...
1903
	if (!isset($context['action']))
1904
		$context['action'] = 'action=profile;area=notification;sa=alerts;u=' . $memID;
1905
1906
	// What options are set
1907
	loadThemeOptions($memID);
1908
	loadJavaScriptFile('alertSettings.js', array('minimize' => true), 'smf_alertSettings');
1909
1910
	// Now load all the values for this user.
1911
	require_once($sourcedir . '/Subs-Notify.php');
1912
	$prefs = getNotifyPrefs($memID, '', $memID != 0);
1913
1914
	$context['alert_prefs'] = !empty($prefs[$memID]) ? $prefs[$memID] : array();
1915
1916
	$context['member'] += array(
1917
		'alert_timeout' => isset($context['alert_prefs']['alert_timeout']) ? $context['alert_prefs']['alert_timeout'] : 10,
1918
		'notify_announcements' => isset($context['alert_prefs']['announcements']) ? $context['alert_prefs']['announcements'] : 0,
1919
	);
1920
1921
	// Now for the exciting stuff.
1922
	// We have groups of items, each item has both an alert and an email key as well as an optional help string.
1923
	// Valid values for these keys are 'always', 'yes', 'never'; if using always or never you should add a help string.
1924
	$alert_types = array(
1925
		'board' => array(
1926
			'topic_notify' => array('alert' => 'yes', 'email' => 'yes'),
1927
			'board_notify' => array('alert' => 'yes', 'email' => 'yes'),
1928
		),
1929
		'msg' => array(
1930
			'msg_mention' => array('alert' => 'yes', 'email' => 'yes'),
1931
			'msg_quote' => array('alert' => 'yes', 'email' => 'yes'),
1932
			'msg_like' => array('alert' => 'yes', 'email' => 'never'),
1933
			'unapproved_reply' => array('alert' => 'yes', 'email' => 'yes'),
1934
		),
1935
		'pm' => array(
1936
			'pm_new' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_read', 'is_board' => false)),
1937
			'pm_reply' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_send', 'is_board' => false)),
1938
		),
1939
		'groupr' => array(
1940
			'groupr_approved' => array('alert' => 'always', 'email' => 'yes'),
1941
			'groupr_rejected' => array('alert' => 'always', 'email' => 'yes'),
1942
		),
1943
		'moderation' => array(
1944
			'unapproved_post' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'approve_posts', 'is_board' => true)),
1945
			'msg_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1946
			'msg_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1947
			'member_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1948
			'member_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1949
		),
1950
		'members' => array(
1951
			'member_register' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1952
			'request_group' => array('alert' => 'yes', 'email' => 'yes'),
1953
			'warn_any' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'issue_warning', 'is_board' => false)),
1954
			'buddy_request'  => array('alert' => 'yes', 'email' => 'never'),
1955
			'birthday'  => array('alert' => 'yes', 'email' => 'yes'),
1956
		),
1957
		'calendar' => array(
1958
			'event_new' => array('alert' => 'yes', 'email' => 'yes', 'help' => 'alert_event_new'),
1959
		),
1960
		'paidsubs' => array(
1961
			'paidsubs_expiring' => array('alert' => 'yes', 'email' => 'yes'),
1962
		),
1963
	);
1964
	$group_options = array(
1965
		'board' => array(
1966
			array('check', 'msg_auto_notify', 'label' => 'after'),
1967
			array('check', 'msg_receive_body', 'label' => 'after'),
1968
			array('select', 'msg_notify_pref', 'label' => 'before', 'opts' => array(
1969
				0 => $txt['alert_opt_msg_notify_pref_nothing'],
1970
				1 => $txt['alert_opt_msg_notify_pref_instant'],
1971
				2 => $txt['alert_opt_msg_notify_pref_first'],
1972
				3 => $txt['alert_opt_msg_notify_pref_daily'],
1973
				4 => $txt['alert_opt_msg_notify_pref_weekly'],
1974
			)),
1975
			array('select', 'msg_notify_type', 'label' => 'before', 'opts' => array(
1976
				1 => $txt['notify_send_type_everything'],
1977
				2 => $txt['notify_send_type_everything_own'],
1978
				3 => $txt['notify_send_type_only_replies'],
1979
				4 => $txt['notify_send_type_nothing'],
1980
			)),
1981
		),
1982
		'pm' => array(
1983
			array('select', 'pm_notify', 'label' => 'before', 'opts' => array(
1984
				1 => $txt['email_notify_all'],
1985
				2 => $txt['email_notify_buddies'],
1986
			)),
1987
		),
1988
	);
1989
1990
	// There are certain things that are disabled at the group level.
1991
	if (empty($modSettings['cal_enabled']))
1992
		unset($alert_types['calendar']);
1993
1994
	// Disable paid subscriptions at group level if they're disabled
1995
	if (empty($modSettings['paid_enabled']))
1996
		unset($alert_types['paidsubs']);
1997
1998
	// Disable membergroup requests at group level if they're disabled
1999
	if (empty($modSettings['show_group_membership']))
2000
		unset($alert_types['groupr'], $alert_types['members']['request_group']);
2001
2002
	// Disable mentions if they're disabled
2003
	if (empty($modSettings['enable_mentions']))
2004
		unset($alert_types['msg']['msg_mention']);
2005
2006
	// Disable likes if they're disabled
2007
	if (empty($modSettings['enable_likes']))
2008
		unset($alert_types['msg']['msg_like']);
2009
2010
	// Disable buddy requests if they're disabled
2011
	if (empty($modSettings['enable_buddylist']))
2012
		unset($alert_types['members']['buddy_request']);
2013
2014
	// Now, now, we could pass this through global but we should really get into the habit of
2015
	// passing content to hooks, not expecting hooks to splatter everything everywhere.
2016
	call_integration_hook('integrate_alert_types', array(&$alert_types, &$group_options));
2017
2018
	// Now we have to do some permissions testing - but only if we're not loading this from the admin center
2019
	if (!empty($memID))
2020
	{
2021
		require_once($sourcedir . '/Subs-Members.php');
2022
		$perms_cache = array();
2023
		$request = $smcFunc['db_query']('', '
2024
			SELECT COUNT(*)
2025
			FROM {db_prefix}group_moderators
2026
			WHERE id_member = {int:memID}',
2027
			array(
2028
				'memID' => $memID,
2029
			)
2030
		);
2031
2032
		list ($can_mod) = $smcFunc['db_fetch_row']($request);
2033
2034
		if (!isset($perms_cache['manage_membergroups']))
2035
		{
2036
			$members = membersAllowedTo('manage_membergroups');
2037
			$perms_cache['manage_membergroups'] = in_array($memID, $members);
2038
		}
2039
2040
		if (!($perms_cache['manage_membergroups'] || $can_mod != 0))
2041
			unset($alert_types['members']['request_group']);
2042
2043
		foreach ($alert_types as $group => $items)
2044
		{
2045
			foreach ($items as $alert_key => $alert_value)
2046
			{
2047
				if (!isset($alert_value['permission']))
2048
					continue;
2049
				if (!isset($perms_cache[$alert_value['permission']['name']]))
2050
				{
2051
					$in_board = !empty($alert_value['permission']['is_board']) ? 0 : null;
2052
					$members = membersAllowedTo($alert_value['permission']['name'], $in_board);
2053
					$perms_cache[$alert_value['permission']['name']] = in_array($memID, $members);
2054
				}
2055
2056
				if (!$perms_cache[$alert_value['permission']['name']])
2057
					unset ($alert_types[$group][$alert_key]);
2058
			}
2059
2060
			if (empty($alert_types[$group]))
2061
				unset ($alert_types[$group]);
2062
		}
2063
	}
2064
2065
	// And finally, exporting it to be useful later.
2066
	$context['alert_types'] = $alert_types;
2067
	$context['alert_group_options'] = $group_options;
2068
2069
	$context['alert_bits'] = array(
2070
		'alert' => 0x01,
2071
		'email' => 0x02,
2072
	);
2073
2074
	if (isset($_POST['notify_submit']))
2075
	{
2076
		checkSession();
2077
		validateToken($context['token_check'], 'post');
2078
2079
		// We need to step through the list of valid settings and figure out what the user has set.
2080
		$update_prefs = array();
2081
2082
		// Now the group level options
2083
		foreach ($context['alert_group_options'] as $opt_group => $group)
2084
		{
2085
			foreach ($group as $this_option)
2086
			{
2087
				switch ($this_option[0])
2088
				{
2089
					case 'check':
2090
						$update_prefs[$this_option[1]] = !empty($_POST['opt_' . $this_option[1]]) ? 1 : 0;
2091
						break;
2092
					case 'select':
2093
						if (isset($_POST['opt_' . $this_option[1]], $this_option['opts'][$_POST['opt_' . $this_option[1]]]))
2094
							$update_prefs[$this_option[1]] = $_POST['opt_' . $this_option[1]];
2095
						else
2096
						{
2097
							// We didn't have a sane value. Let's grab the first item from the possibles.
2098
							$keys = array_keys($this_option['opts']);
2099
							$first = array_shift($keys);
2100
							$update_prefs[$this_option[1]] = $first;
2101
						}
2102
						break;
2103
				}
2104
			}
2105
		}
2106
2107
		// Now the individual options
2108
		foreach ($context['alert_types'] as $alert_group => $items)
2109
		{
2110
			foreach ($items as $item_key => $this_options)
2111
			{
2112
				$this_value = 0;
2113
				foreach ($context['alert_bits'] as $type => $bitvalue)
2114
				{
2115
					if ($this_options[$type] == 'yes' && !empty($_POST[$type . '_' . $item_key]) || $this_options[$type] == 'always')
2116
						$this_value |= $bitvalue;
2117
				}
2118
				if (!isset($context['alert_prefs'][$item_key]) || $context['alert_prefs'][$item_key] != $this_value)
2119
					$update_prefs[$item_key] = $this_value;
2120
			}
2121
		}
2122
2123
		if (!empty($_POST['opt_alert_timeout']))
2124
			$update_prefs['alert_timeout'] = $context['member']['alert_timeout'] = (int) $_POST['opt_alert_timeout'];
2125
2126
		if (!empty($_POST['notify_announcements']))
2127
			$update_prefs['announcements'] = $context['member']['notify_announcements'] = (int) $_POST['notify_announcements'];
2128
2129
		setNotifyPrefs((int) $memID, $update_prefs);
2130
		foreach ($update_prefs as $pref => $value)
2131
			$context['alert_prefs'][$pref] = $value;
2132
2133
		makeNotificationChanges($memID);
2134
2135
		$context['profile_updated'] = $txt['profile_updated_own'];
2136
	}
2137
2138
	createToken($context['token_check'], 'post');
2139
}
2140
2141
/**
2142
 * Marks all alerts as read for the specified user
2143
 *
2144
 * @param int $memID The ID of the member
2145
 */
2146
function alert_markread($memID)
2147
{
2148
	global $context, $db_show_debug, $smcFunc;
2149
2150
	// We do not want to output debug information here.
2151
	$db_show_debug = false;
2152
2153
	// We only want to output our little layer here.
2154
	$context['template_layers'] = array();
2155
	$context['sub_template'] = 'alerts_all_read';
2156
2157
	loadLanguage('Alerts');
2158
2159
	// Now we're all set up.
2160
	is_not_guest();
2161
	if (!$context['user']['is_owner'])
2162
		fatal_error('no_access');
2163
2164
	checkSession('get');
2165
2166
	// Assuming we're here, mark everything as read and head back.
2167
	// We only spit back the little layer because this should be called AJAXively.
2168
	$smcFunc['db_query']('', '
2169
		UPDATE {db_prefix}user_alerts
2170
		SET is_read = {int:now}
2171
		WHERE id_member = {int:current_member}
2172
			AND is_read = 0',
2173
		array(
2174
			'now' => time(),
2175
			'current_member' => $memID,
2176
		)
2177
	);
2178
2179
	updateMemberData($memID, array('alerts' => 0));
2180
}
2181
2182
/**
2183
 * Marks a group of alerts as un/read
2184
 *
2185
 * @param int $memID The user ID.
2186
 * @param array|integer $toMark The ID of a single alert or an array of IDs. The function will convert single integers to arrays for better handling.
2187
 * @param integer $read To mark as read or unread, 1 for read, 0 or any other value different than 1 for unread.
2188
 * @return integer How many alerts remain unread
0 ignored issues
show
Documentation introduced by
Should the return type not be false|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2189
 */
2190
function alert_mark($memID, $toMark, $read = 0)
2191
{
2192
	global $smcFunc;
2193
2194
	if (empty($toMark) || empty($memID))
2195
		return false;
2196
2197
	$toMark = (array) $toMark;
2198
2199
	$smcFunc['db_query']('', '
2200
		UPDATE {db_prefix}user_alerts
2201
		SET is_read = {int:read}
2202
		WHERE id_alert IN({array_int:toMark})',
2203
		array(
2204
			'read' => $read == 1 ? time() : 0,
2205
			'toMark' => $toMark,
2206
		)
2207
	);
2208
2209
	// Gotta know how many unread alerts are left.
2210
	$count = alert_count($memID, true);
2211
2212
	updateMemberData($memID, array('alerts' => $count));
2213
2214
	// Might want to know this.
2215
	return $count;
2216
}
2217
2218
/**
2219
 * Deletes a single or a group of alerts by ID
2220
 *
2221
 * @param int|array The ID of a single alert to delete or an array containing the IDs of multiple alerts. The function will convert integers into an array for better handling.
2222
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2223
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
0 ignored issues
show
Documentation introduced by
Should the return type not be false|integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2224
 */
2225
function alert_delete($toDelete, $memID = false)
2226
{
2227
	global $smcFunc;
2228
2229
	if (empty($toDelete))
2230
		return false;
2231
2232
	$toDelete = (array) $toDelete;
2233
2234
	$smcFunc['db_query']('', '
2235
		DELETE FROM {db_prefix}user_alerts
2236
		WHERE id_alert IN({array_int:toDelete})',
2237
		array(
2238
			'toDelete' => $toDelete,
2239
		)
2240
	);
2241
2242
	// Gotta know how many unread alerts are left.
2243
	if ($memID)
2244
	{
2245
		$count = alert_count($memID, true);
0 ignored issues
show
Bug introduced by
It seems like $memID defined by parameter $memID on line 2225 can also be of type boolean; however, alert_count() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2246
2247
		updateMemberData($memID, array('alerts' => $count));
2248
2249
		// Might want to know this.
2250
		return $count;
2251
	}
2252
}
2253
2254
/**
2255
 * Counts how many alerts a user has - either unread or all depending on $unread
2256
 *
2257
 * @param int $memID The user ID.
2258
 * @param bool $unread Whether to only count unread alerts.
2259
 * @return int The number of requested alerts
2260
 */
2261
function alert_count($memID, $unread = false)
2262
{
2263
	global $smcFunc;
2264
2265
	if (empty($memID))
2266
		return false;
2267
2268
	$request = $smcFunc['db_query']('', '
2269
		SELECT id_alert
2270
		FROM {db_prefix}user_alerts
2271
		WHERE id_member = {int:id_member}
2272
			'.($unread ? '
2273
			AND is_read = 0' : ''),
2274
		array(
2275
			'id_member' => $memID,
2276
		)
2277
	);
2278
2279
	$count = $smcFunc['db_num_rows']($request);
2280
	$smcFunc['db_free_result']($request);
2281
2282
	return $count;
2283
}
2284
2285
/**
2286
 * Handles alerts related to topics and posts
2287
 *
2288
 * @param int $memID The ID of the member
2289
 */
2290
function alert_notifications_topics($memID)
2291
{
2292
	global $txt, $scripturl, $context, $modSettings, $sourcedir;
2293
2294
	// Because of the way this stuff works, we want to do this ourselves.
2295 View Code Duplication
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
2296
	{
2297
		checkSession();
2298
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2299
2300
		makeNotificationChanges($memID);
2301
		$context['profile_updated'] = $txt['profile_updated_own'];
2302
	}
2303
2304
	// Now set up for the token check.
2305
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2306
	createToken($context['token_check'], 'post');
2307
2308
	// Gonna want this for the list.
2309
	require_once($sourcedir . '/Subs-List.php');
2310
2311
	// Do the topic notifications.
2312
	$listOptions = array(
2313
		'id' => 'topic_notification_list',
2314
		'width' => '100%',
2315
		'items_per_page' => $modSettings['defaultMaxListItems'],
2316
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2317
		'no_items_align' => 'left',
2318
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2319
		'default_sort_col' => 'last_post',
2320
		'get_items' => array(
2321
			'function' => 'list_getTopicNotifications',
2322
			'params' => array(
2323
				$memID,
2324
			),
2325
		),
2326
		'get_count' => array(
2327
			'function' => 'list_getTopicNotificationCount',
2328
			'params' => array(
2329
				$memID,
2330
			),
2331
		),
2332
		'columns' => array(
2333
			'subject' => array(
2334
				'header' => array(
2335
					'value' => $txt['notifications_topics'],
2336
					'class' => 'lefttext',
2337
				),
2338
				'data' => array(
2339
					'function' => function($topic) use ($txt)
2340
					{
2341
						$link = $topic['link'];
2342
2343
						if ($topic['new'])
2344
							$link .= ' <a href="' . $topic['new_href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2345
2346
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2347
2348
						return $link;
2349
					},
2350
				),
2351
				'sort' => array(
2352
					'default' => 'ms.subject',
2353
					'reverse' => 'ms.subject DESC',
2354
				),
2355
			),
2356
			'started_by' => array(
2357
				'header' => array(
2358
					'value' => $txt['started_by'],
2359
					'class' => 'lefttext',
2360
				),
2361
				'data' => array(
2362
					'db' => 'poster_link',
2363
				),
2364
				'sort' => array(
2365
					'default' => 'real_name_col',
2366
					'reverse' => 'real_name_col DESC',
2367
				),
2368
			),
2369
			'last_post' => array(
2370
				'header' => array(
2371
					'value' => $txt['last_post'],
2372
					'class' => 'lefttext',
2373
				),
2374
				'data' => array(
2375
					'sprintf' => array(
2376
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2377
						'params' => array(
2378
							'updated' => false,
2379
							'poster_updated_link' => false,
2380
						),
2381
					),
2382
				),
2383
				'sort' => array(
2384
					'default' => 'ml.id_msg DESC',
2385
					'reverse' => 'ml.id_msg',
2386
				),
2387
			),
2388
			'alert' => array(
2389
				'header' => array(
2390
					'value' => $txt['notify_what_how'],
2391
					'class' => 'lefttext',
2392
				),
2393
				'data' => array(
2394
					'function' => function($topic) use ($txt)
2395
					{
2396
						$pref = $topic['notify_pref'];
2397
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2398
						return $txt['notify_topic_' . $mode];
2399
					},
2400
				),
2401
			),
2402
			'delete' => array(
2403
				'header' => array(
2404
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2405
					'style' => 'width: 4%;',
2406
					'class' => 'centercol',
2407
				),
2408
				'data' => array(
2409
					'sprintf' => array(
2410
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d">',
2411
						'params' => array(
2412
							'id' => false,
2413
						),
2414
					),
2415
					'class' => 'centercol',
2416
				),
2417
			),
2418
		),
2419
		'form' => array(
2420
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2421
			'include_sort' => true,
2422
			'include_start' => true,
2423
			'hidden_fields' => array(
2424
				'u' => $memID,
2425
				'sa' => $context['menu_item_selected'],
2426
				$context['session_var'] => $context['session_id'],
2427
			),
2428
			'token' => $context['token_check'],
2429
		),
2430
		'additional_rows' => array(
2431
			array(
2432
				'position' => 'bottom_of_list',
2433
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button" />
2434
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2435
				'class' => 'floatright',
2436
			),
2437
		),
2438
	);
2439
2440
	// Create the notification list.
2441
	createList($listOptions);
2442
}
2443
2444
/**
2445
 * Handles preferences related to board-level notifications
2446
 *
2447
 * @param int $memID The ID of the member
2448
 */
2449
function alert_notifications_boards($memID)
2450
{
2451
	global $txt, $scripturl, $context, $sourcedir;
2452
2453
	// Because of the way this stuff works, we want to do this ourselves.
2454 View Code Duplication
	if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
0 ignored issues
show
Bug introduced by
The variable $_POSt seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
2455
	{
2456
		checkSession();
2457
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2458
2459
		makeNotificationChanges($memID);
2460
		$context['profile_updated'] = $txt['profile_updated_own'];
2461
	}
2462
2463
	// Now set up for the token check.
2464
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2465
	createToken($context['token_check'], 'post');
2466
2467
	// Gonna want this for the list.
2468
	require_once($sourcedir . '/Subs-List.php');
2469
2470
	// Fine, start with the board list.
2471
	$listOptions = array(
2472
		'id' => 'board_notification_list',
2473
		'width' => '100%',
2474
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2475
		'no_items_align' => 'left',
2476
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2477
		'default_sort_col' => 'board_name',
2478
		'get_items' => array(
2479
			'function' => 'list_getBoardNotifications',
2480
			'params' => array(
2481
				$memID,
2482
			),
2483
		),
2484
		'columns' => array(
2485
			'board_name' => array(
2486
				'header' => array(
2487
					'value' => $txt['notifications_boards'],
2488
					'class' => 'lefttext',
2489
				),
2490
				'data' => array(
2491
					'function' => function($board) use ($txt)
2492
					{
2493
						$link = $board['link'];
2494
2495
						if ($board['new'])
2496
							$link .= ' <a href="' . $board['href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2497
2498
						return $link;
2499
					},
2500
				),
2501
				'sort' => array(
2502
					'default' => 'name',
2503
					'reverse' => 'name DESC',
2504
				),
2505
			),
2506
			'alert' => array(
2507
				'header' => array(
2508
					'value' => $txt['notify_what_how'],
2509
					'class' => 'lefttext',
2510
				),
2511
				'data' => array(
2512
					'function' => function($board) use ($txt)
2513
					{
2514
						$pref = $board['notify_pref'];
2515
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2516
						return $txt['notify_board_' . $mode];
2517
					},
2518
				),
2519
			),
2520
			'delete' => array(
2521
				'header' => array(
2522
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2523
					'style' => 'width: 4%;',
2524
					'class' => 'centercol',
2525
				),
2526
				'data' => array(
2527
					'sprintf' => array(
2528
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d">',
2529
						'params' => array(
2530
							'id' => false,
2531
						),
2532
					),
2533
					'class' => 'centercol',
2534
				),
2535
			),
2536
		),
2537
		'form' => array(
2538
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2539
			'include_sort' => true,
2540
			'include_start' => true,
2541
			'hidden_fields' => array(
2542
				'u' => $memID,
2543
				'sa' => $context['menu_item_selected'],
2544
				$context['session_var'] => $context['session_id'],
2545
			),
2546
			'token' => $context['token_check'],
2547
		),
2548
		'additional_rows' => array(
2549
			array(
2550
				'position' => 'bottom_of_list',
2551
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button">
2552
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2553
				'class' => 'floatright',
2554
			),
2555
		),
2556
	);
2557
2558
	// Create the board notification list.
2559
	createList($listOptions);
2560
}
2561
2562
/**
2563
 * Determins how many topics a user has requested notifications for
2564
 *
2565
 * @param int $memID The ID of the member
2566
 * @return int The number of topic notifications for this user
2567
 */
2568
function list_getTopicNotificationCount($memID)
2569
{
2570
	global $smcFunc, $user_info, $modSettings;
2571
2572
	$request = $smcFunc['db_query']('', '
2573
		SELECT COUNT(*)
2574
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2575
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2576
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2577
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2578
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2579
			AND t.approved = {int:is_approved}' : ''),
2580
		array(
2581
			'selected_member' => $memID,
2582
			'is_approved' => 1,
2583
		)
2584
	);
2585
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2586
	$smcFunc['db_free_result']($request);
2587
2588
	return (int) $totalNotifications;
2589
}
2590
2591
/**
2592
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2593
 *
2594
 * @param int $start Which item to start with (for pagination purposes)
2595
 * @param int $items_per_page How many items to display on each page
2596
 * @param string $sort A string indicating how to sort the results
2597
 * @param int $memID The ID of the member
2598
 * @return array An array of information about the topics a user has subscribed to
2599
 */
2600
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2601
{
2602
	global $smcFunc, $scripturl, $user_info, $modSettings, $sourcedir;
2603
2604
	require_once($sourcedir . '/Subs-Notify.php');
2605
	$prefs = getNotifyPrefs($memID);
2606
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2607
2608
	// All the topics with notification on...
2609
	$request = $smcFunc['db_query']('', '
2610
		SELECT
2611
			COALESCE(lt.id_msg, COALESCE(lmr.id_msg, -1)) + 1 AS new_from, b.id_board, b.name,
2612
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2613
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2614
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2615
			lt.unwatched
2616
		FROM {db_prefix}log_notify AS ln
2617
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2618
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2619
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2620
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2621
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2622
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2623
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2624
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2625
		WHERE ln.id_member = {int:selected_member}
2626
		ORDER BY {raw:sort}
2627
		LIMIT {int:offset}, {int:items_per_page}',
2628
		array(
2629
			'current_member' => $user_info['id'],
2630
			'is_approved' => 1,
2631
			'selected_member' => $memID,
2632
			'sort' => $sort,
2633
			'offset' => $start,
2634
			'items_per_page' => $items_per_page,
2635
		)
2636
	);
2637
	$notification_topics = array();
2638
	while ($row = $smcFunc['db_fetch_assoc']($request))
2639
	{
2640
		censorText($row['subject']);
2641
2642
		$notification_topics[] = array(
2643
			'id' => $row['id_topic'],
2644
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2645
			'poster_updated_link' => empty($row['id_member_updated']) ? $row['last_real_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member_updated'] . '">' . $row['last_real_name'] . '</a>',
2646
			'subject' => $row['subject'],
2647
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2648
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2649
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2650
			'new_from' => $row['new_from'],
2651
			'updated' => timeformat($row['poster_time']),
2652
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2653
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2654
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2655
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2656
			'unwatched' => $row['unwatched'],
2657
		);
2658
	}
2659
	$smcFunc['db_free_result']($request);
2660
2661
	return $notification_topics;
2662
}
2663
2664
/**
2665
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2666
 *
2667
 * @param int $start Which item to start with (not used here)
2668
 * @param int $items_per_page How many items to show on each page (not used here)
2669
 * @param string $sort A string indicating how to sort the results
2670
 * @param int $memID The ID of the member
2671
 * @return array An array of information about all the boards a user is subscribed to
2672
 */
2673
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2674
{
2675
	global $smcFunc, $scripturl, $user_info, $sourcedir;
2676
2677
	require_once($sourcedir . '/Subs-Notify.php');
2678
	$prefs = getNotifyPrefs($memID);
2679
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2680
2681
	$request = $smcFunc['db_query']('', '
2682
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2683
		FROM {db_prefix}log_notify AS ln
2684
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2685
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2686
		WHERE ln.id_member = {int:selected_member}
2687
			AND {query_see_board}
2688
		ORDER BY {raw:sort}',
2689
		array(
2690
			'current_member' => $user_info['id'],
2691
			'selected_member' => $memID,
2692
			'sort' => $sort,
2693
		)
2694
	);
2695
	$notification_boards = array();
2696
	while ($row = $smcFunc['db_fetch_assoc']($request))
2697
		$notification_boards[] = array(
2698
			'id' => $row['id_board'],
2699
			'name' => $row['name'],
2700
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2701
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2702
			'new' => $row['board_read'] < $row['id_msg_updated'],
2703
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2704
		);
2705
	$smcFunc['db_free_result']($request);
2706
2707
	return $notification_boards;
2708
}
2709
2710
/**
2711
 * Loads the theme options for a user
2712
 *
2713
 * @param int $memID The ID of the member
2714
 */
2715
function loadThemeOptions($memID)
2716
{
2717
	global $context, $options, $cur_profile, $smcFunc;
2718
2719 View Code Duplication
	if (isset($_POST['default_options']))
2720
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2721
2722
	if ($context['user']['is_owner'])
2723
	{
2724
		$context['member']['options'] = $options;
2725
		if (isset($_POST['options']) && is_array($_POST['options']))
2726
			foreach ($_POST['options'] as $k => $v)
2727
				$context['member']['options'][$k] = $v;
2728
	}
2729
	else
2730
	{
2731
		$request = $smcFunc['db_query']('', '
2732
			SELECT id_member, variable, value
2733
			FROM {db_prefix}themes
2734
			WHERE id_theme IN (1, {int:member_theme})
2735
				AND id_member IN (-1, {int:selected_member})',
2736
			array(
2737
				'member_theme' => (int) $cur_profile['id_theme'],
2738
				'selected_member' => $memID,
2739
			)
2740
		);
2741
		$temp = array();
2742
		while ($row = $smcFunc['db_fetch_assoc']($request))
2743
		{
2744
			if ($row['id_member'] == -1)
2745
			{
2746
				$temp[$row['variable']] = $row['value'];
2747
				continue;
2748
			}
2749
2750
			if (isset($_POST['options'][$row['variable']]))
2751
				$row['value'] = $_POST['options'][$row['variable']];
2752
			$context['member']['options'][$row['variable']] = $row['value'];
2753
		}
2754
		$smcFunc['db_free_result']($request);
2755
2756
		// Load up the default theme options for any missing.
2757
		foreach ($temp as $k => $v)
2758
		{
2759
			if (!isset($context['member']['options'][$k]))
2760
				$context['member']['options'][$k] = $v;
2761
		}
2762
	}
2763
}
2764
2765
/**
2766
 * Handles the "ignored boards" section of the profile (if enabled)
2767
 *
2768
 * @param int $memID The ID of the member
2769
 */
2770
function ignoreboards($memID)
2771
{
2772
	global $context, $modSettings, $smcFunc, $cur_profile, $sourcedir;
2773
2774
	// Have the admins enabled this option?
2775
	if (empty($modSettings['allow_ignore_boards']))
2776
		fatal_lang_error('ignoreboards_disallowed', 'user');
2777
2778
	// Find all the boards this user is allowed to see.
2779
	$request = $smcFunc['db_query']('order_by_board_order', '
2780
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
2781
			'. (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
2782
		FROM {db_prefix}boards AS b
2783
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2784
		WHERE {query_see_board}
2785
			AND redirect = {string:empty_string}',
2786
		array(
2787
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
2788
			'empty_string' => '',
2789
		)
2790
	);
2791
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
2792
	$context['categories'] = array();
2793 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2794
	{
2795
		// This category hasn't been set up yet..
2796
		if (!isset($context['categories'][$row['id_cat']]))
2797
			$context['categories'][$row['id_cat']] = array(
2798
				'id' => $row['id_cat'],
2799
				'name' => $row['cat_name'],
2800
				'boards' => array()
2801
			);
2802
2803
		// Set this board up, and let the template know when it's a child.  (indent them..)
2804
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
2805
			'id' => $row['id_board'],
2806
			'name' => $row['name'],
2807
			'child_level' => $row['child_level'],
2808
			'selected' => $row['is_ignored'],
2809
		);
2810
	}
2811
	$smcFunc['db_free_result']($request);
2812
2813
	require_once($sourcedir . '/Subs-Boards.php');
2814
	sortCategories($context['categories']);
2815
2816
	// Now, let's sort the list of categories into the boards for templates that like that.
2817
	$temp_boards = array();
2818 View Code Duplication
	foreach ($context['categories'] as $category)
2819
	{
2820
		// Include a list of boards per category for easy toggling.
2821
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
2822
2823
		$temp_boards[] = array(
2824
			'name' => $category['name'],
2825
			'child_ids' => array_keys($category['boards'])
2826
		);
2827
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
2828
	}
2829
2830
	$max_boards = ceil(count($temp_boards) / 2);
2831
	if ($max_boards == 1)
2832
		$max_boards = 2;
2833
2834
	// Now, alternate them so they can be shown left and right ;).
2835
	$context['board_columns'] = array();
2836 View Code Duplication
	for ($i = 0; $i < $max_boards; $i++)
2837
	{
2838
		$context['board_columns'][] = $temp_boards[$i];
2839
		if (isset($temp_boards[$i + $max_boards]))
2840
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
2841
		else
2842
			$context['board_columns'][] = array();
2843
	}
2844
2845
	loadThemeOptions($memID);
2846
}
2847
2848
/**
2849
 * Load all the languages for the profile
2850
 * .
2851
 * @return bool Whether or not the forum has multiple languages installed
2852
 */
2853
function profileLoadLanguages()
2854
{
2855
	global $context;
2856
2857
	$context['profile_languages'] = array();
2858
2859
	// Get our languages!
2860
	getLanguages();
2861
2862
	// Setup our languages.
2863
	foreach ($context['languages'] as $lang)
2864
	{
2865
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
2866
	}
2867
	ksort($context['profile_languages']);
2868
2869
	// Return whether we should proceed with this.
2870
	return count($context['profile_languages']) > 1 ? true : false;
2871
}
2872
2873
/**
2874
 * Handles the "manage groups" section of the profile
2875
 *
2876
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2877
 */
2878
function profileLoadGroups()
2879
{
2880
	global $cur_profile, $txt, $context, $smcFunc, $user_settings;
2881
2882
	$context['member_groups'] = array(
2883
		0 => array(
2884
			'id' => 0,
2885
			'name' => $txt['no_primary_membergroup'],
2886
			'is_primary' => $cur_profile['id_group'] == 0,
2887
			'can_be_additional' => false,
2888
			'can_be_primary' => true,
2889
		)
2890
	);
2891
	$curGroups = explode(',', $cur_profile['additional_groups']);
2892
2893
	// Load membergroups, but only those groups the user can assign.
2894
	$request = $smcFunc['db_query']('', '
2895
		SELECT group_name, id_group, hidden
2896
		FROM {db_prefix}membergroups
2897
		WHERE id_group != {int:moderator_group}
2898
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
2899
			AND group_type != {int:is_protected}') . '
2900
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
2901
		array(
2902
			'moderator_group' => 3,
2903
			'min_posts' => -1,
2904
			'is_protected' => 1,
2905
			'newbie_group' => 4,
2906
		)
2907
	);
2908
	while ($row = $smcFunc['db_fetch_assoc']($request))
2909
	{
2910
		// We should skip the administrator group if they don't have the admin_forum permission!
2911
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
2912
			continue;
2913
2914
		$context['member_groups'][$row['id_group']] = array(
2915
			'id' => $row['id_group'],
2916
			'name' => $row['group_name'],
2917
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
2918
			'is_additional' => in_array($row['id_group'], $curGroups),
2919
			'can_be_additional' => true,
2920
			'can_be_primary' => $row['hidden'] != 2,
2921
		);
2922
	}
2923
	$smcFunc['db_free_result']($request);
2924
2925
	$context['member']['group_id'] = $user_settings['id_group'];
2926
2927
	return true;
2928
}
2929
2930
/**
2931
 * Load key signature context data.
2932
 *
2933
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2934
 */
2935
function profileLoadSignatureData()
2936
{
2937
	global $modSettings, $context, $txt, $cur_profile, $memberContext;
2938
2939
	// Signature limits.
2940
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
2941
	$sig_limits = explode(',', $sig_limits);
2942
2943
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
2944
	$context['signature_limits'] = array(
2945
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
2946
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
2947
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
2948
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
2949
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
2950
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
2951
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
2952
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
2953
	);
2954
	// Kept this line in for backwards compatibility!
2955
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
2956
	// Warning message for signature image limits?
2957
	$context['signature_warning'] = '';
2958
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
2959
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
2960
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
2961
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_' . ($context['signature_limits']['max_image_width'] ? 'width' : 'height')], $context['signature_limits'][$context['signature_limits']['max_image_width'] ? 'max_image_width' : 'max_image_height']);
2962
2963
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_character_set'] == 'UTF-8' || function_exists('iconv'))));
2964
2965
	if (empty($context['do_preview']))
2966
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
2967
	else
2968
	{
2969
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
2970
		$validation = profileValidateSignature($signature);
2971
		if (empty($context['post_errors']))
2972
		{
2973
			loadLanguage('Errors');
2974
			$context['post_errors'] = array();
2975
		}
2976
		$context['post_errors'][] = 'signature_not_yet_saved';
2977
		if ($validation !== true && $validation !== false)
2978
			$context['post_errors'][] = $validation;
2979
2980
		censorText($context['member']['signature']);
2981
		$context['member']['current_signature'] = $context['member']['signature'];
2982
		censorText($signature);
2983
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']]);
2984
		$context['member']['signature'] = $_POST['signature'];
2985
	}
2986
2987
	// Load the spell checker?
2988 View Code Duplication
	if ($context['show_spellchecking'])
2989
		loadJavaScriptFile('spellcheck.js', array('defer' => false, 'minimize' => true), 'smf_spellcheck');
2990
2991
	return true;
2992
}
2993
2994
/**
2995
 * Load avatar context data.
2996
 *
2997
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2998
 */
2999
function profileLoadAvatarData()
3000
{
3001
	global $context, $cur_profile, $modSettings, $scripturl;
3002
3003
	$context['avatar_url'] = $modSettings['avatar_url'];
3004
3005
	// Default context.
3006
	$context['member']['avatar'] += array(
3007
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
3008
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
3009
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3010
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3011
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3012
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']) || !empty($modSettings['gravatarOverride']),
3013
	);
3014
3015
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
3016
	{
3017
		$context['member']['avatar'] += array(
3018
			'choice' => 'gravatar',
3019
			'server_pic' => 'blank.png',
3020
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || !empty($modSettings['gravatarOverride']) ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
3021
		);
3022
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
3023
	}
3024
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
3025
	{
3026
		$context['member']['avatar'] += array(
3027
			'choice' => 'upload',
3028
			'server_pic' => 'blank.png',
3029
			'external' => 'http://'
3030
		);
3031
		$context['member']['avatar']['href'] = empty($cur_profile['attachment_type']) ? $scripturl . '?action=dlattach;attach=' . $cur_profile['id_attach'] . ';type=avatar' : $modSettings['custom_avatar_url'] . '/' . $cur_profile['filename'];
3032
	}
3033
	// Use "avatar_original" here so we show what the user entered even if the image proxy is enabled
3034
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
3035
		$context['member']['avatar'] += array(
3036
			'choice' => 'external',
3037
			'server_pic' => 'blank.png',
3038
			'external' => $cur_profile['avatar_original']
3039
		);
3040
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
3041
		$context['member']['avatar'] += array(
3042
			'choice' => 'server_stored',
3043
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3044
			'external' => 'http://'
3045
		);
3046
	else
3047
		$context['member']['avatar'] += array(
3048
			'choice' => 'none',
3049
			'server_pic' => 'blank.png',
3050
			'external' => 'http://'
3051
		);
3052
3053
	// Get a list of all the avatars.
3054
	if ($context['member']['avatar']['allow_server_stored'])
3055
	{
3056
		$context['avatar_list'] = array();
3057
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3058
	}
3059
	else
3060
		$context['avatars'] = array();
3061
3062
	// Second level selected avatar...
3063
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3064
	return !empty($context['member']['avatar']['allow_server_stored']) || !empty($context['member']['avatar']['allow_external']) || !empty($context['member']['avatar']['allow_upload']) || !empty($context['member']['avatar']['allow_gravatar']);
3065
}
3066
3067
/**
3068
 * Save a members group.
3069
 *
3070
 * @param int &$value The ID of the (new) primary group
3071
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3072
 */
3073
function profileSaveGroups(&$value)
3074
{
3075
	global $profile_vars, $old_profile, $context, $smcFunc, $cur_profile;
3076
3077
	// Do we need to protect some groups?
3078 View Code Duplication
	if (!allowedTo('admin_forum'))
3079
	{
3080
		$request = $smcFunc['db_query']('', '
3081
			SELECT id_group
3082
			FROM {db_prefix}membergroups
3083
			WHERE group_type = {int:is_protected}',
3084
			array(
3085
				'is_protected' => 1,
3086
			)
3087
		);
3088
		$protected_groups = array(1);
3089
		while ($row = $smcFunc['db_fetch_assoc']($request))
3090
			$protected_groups[] = $row['id_group'];
3091
		$smcFunc['db_free_result']($request);
3092
3093
		$protected_groups = array_unique($protected_groups);
3094
	}
3095
3096
	// The account page allows the change of your id_group - but not to a protected group!
3097
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3098
		$value = (int) $value;
3099
	// ... otherwise it's the old group sir.
3100
	else
3101
		$value = $old_profile['id_group'];
3102
3103
	// Find the additional membergroups (if any)
3104
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3105
	{
3106
		$additional_groups = array();
3107
		foreach ($_POST['additional_groups'] as $group_id)
3108
		{
3109
			$group_id = (int) $group_id;
3110
			if (!empty($group_id) && (empty($protected_groups) || !in_array($group_id, $protected_groups)))
0 ignored issues
show
Bug introduced by
The variable $protected_groups does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3111
				$additional_groups[] = $group_id;
3112
		}
3113
3114
		// Put the protected groups back in there if you don't have permission to take them away.
3115
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3116
		foreach ($old_additional_groups as $group_id)
3117
		{
3118
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3119
				$additional_groups[] = $group_id;
3120
		}
3121
3122
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3123
		{
3124
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3125
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3126
		}
3127
	}
3128
3129
	// Too often, people remove delete their own account, or something.
3130
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3131
	{
3132
		$stillAdmin = $value == 1 || (isset($additional_groups) && in_array(1, $additional_groups));
0 ignored issues
show
Bug introduced by
The variable $additional_groups does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3133
3134
		// If they would no longer be an admin, look for any other...
3135
		if (!$stillAdmin)
3136
		{
3137
			$request = $smcFunc['db_query']('', '
3138
				SELECT id_member
3139
				FROM {db_prefix}members
3140
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3141
					AND id_member != {int:selected_member}
3142
				LIMIT 1',
3143
				array(
3144
					'admin_group' => 1,
3145
					'selected_member' => $context['id_member'],
3146
				)
3147
			);
3148
			list ($another) = $smcFunc['db_fetch_row']($request);
3149
			$smcFunc['db_free_result']($request);
3150
3151
			if (empty($another))
3152
				fatal_lang_error('at_least_one_admin', 'critical');
3153
		}
3154
	}
3155
3156
	// If we are changing group status, update permission cache as necessary.
3157
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3158
	{
3159 View Code Duplication
		if ($context['user']['is_owner'])
3160
			$_SESSION['mc']['time'] = 0;
3161
		else
3162
			updateSettings(array('settings_updated' => time()));
3163
	}
3164
3165
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3166
	call_integration_hook('integrate_profile_profileSaveGroups', array($value, $additional_groups));
3167
3168
	return true;
3169
}
3170
3171
/**
3172
 * The avatar is incredibly complicated, what with the options... and what not.
3173
 * @todo argh, the avatar here. Take this out of here!
3174
 *
3175
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3176
 * @return bool|string False if success (or if memID is empty and password authentication failed), otherwise a string indicating what error occurred
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
3177
 */
3178
function profileSaveAvatarData(&$value)
3179
{
3180
	global $modSettings, $sourcedir, $smcFunc, $profile_vars, $cur_profile, $context;
3181
3182
	$memID = $context['id_member'];
3183
	if (empty($memID) && !empty($context['password_auth_failed']))
3184
		return false;
3185
3186
	require_once($sourcedir . '/ManageAttachments.php');
3187
3188
	// We're going to put this on a nice custom dir.
3189
	$uploadDir = $modSettings['custom_avatar_dir'];
3190
	$id_folder = 1;
3191
3192
	$downloadedExternalAvatar = false;
3193
	if ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && strlen($_POST['userpicpersonal']) > 7 && !empty($modSettings['avatar_download_external']))
3194
	{
3195
		if (!is_writable($uploadDir))
3196
			fatal_lang_error('attachments_no_write', 'critical');
3197
3198
		$url = parse_url($_POST['userpicpersonal']);
3199
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3200
3201
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3202
		if ($contents != false && $tmpAvatar = fopen($new_filename, 'wb'))
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $contents of type string|false against false; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
3203
		{
3204
			fwrite($tmpAvatar, $contents);
3205
			fclose($tmpAvatar);
3206
3207
			$downloadedExternalAvatar = true;
3208
			$_FILES['attachment']['tmp_name'] = $new_filename;
3209
		}
3210
	}
3211
3212
	// Removes whatever attachment there was before updating
3213
	if ($value == 'none')
3214
	{
3215
		$profile_vars['avatar'] = '';
3216
3217
		// Reset the attach ID.
3218
		$cur_profile['id_attach'] = 0;
3219
		$cur_profile['attachment_type'] = 0;
3220
		$cur_profile['filename'] = '';
3221
3222
		removeAttachments(array('id_member' => $memID));
3223
	}
3224
3225
	// An avatar from the server-stored galleries.
3226
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3227
	{
3228
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3229
		$profile_vars['avatar'] = preg_match('~^([\w _!@%*=\-#()\[\]&.,]+/)?[\w _!@%*=\-#()\[\]&.,]+$~', $profile_vars['avatar']) != 0 && preg_match('/\.\./', $profile_vars['avatar']) == 0 && file_exists($modSettings['avatar_directory'] . '/' . $profile_vars['avatar']) ? ($profile_vars['avatar'] == 'blank.png' ? '' : $profile_vars['avatar']) : '';
3230
3231
		// Clear current profile...
3232
		$cur_profile['id_attach'] = 0;
3233
		$cur_profile['attachment_type'] = 0;
3234
		$cur_profile['filename'] = '';
3235
3236
		// Get rid of their old avatar. (if uploaded.)
3237
		removeAttachments(array('id_member' => $memID));
3238
	}
3239
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3240
	{
3241
		// One wasn't specified, or it's not allowed to use extra email addresses, or it's not a valid one, reset to default Gravatar.
3242
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3243
			$profile_vars['avatar'] = 'gravatar://';
3244
		else
3245
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3246
3247
		// Get rid of their old avatar. (if uploaded.)
3248
		removeAttachments(array('id_member' => $memID));
3249
	}
3250
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3251
	{
3252
		// We need these clean...
3253
		$cur_profile['id_attach'] = 0;
3254
		$cur_profile['attachment_type'] = 0;
3255
		$cur_profile['filename'] = '';
3256
3257
		// Remove any attached avatar...
3258
		removeAttachments(array('id_member' => $memID));
3259
3260
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3261
3262
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3263
			$profile_vars['avatar'] = '';
3264
		// Trying to make us do something we'll regret?
3265
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3266
			return 'bad_avatar_invalid_url';
3267
		// Should we check dimensions?
3268
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3269
		{
3270
			// Now let's validate the avatar.
3271
			$sizes = url_image_size($profile_vars['avatar']);
3272
3273
			if (is_array($sizes) && (($sizes[0] > $modSettings['avatar_max_width_external'] && !empty($modSettings['avatar_max_width_external'])) || ($sizes[1] > $modSettings['avatar_max_height_external'] && !empty($modSettings['avatar_max_height_external']))))
3274
			{
3275
				// Houston, we have a problem. The avatar is too large!!
3276
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3277
					return 'bad_avatar_too_large';
3278
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3279
				{
3280
					// @todo remove this if appropriate
3281
					require_once($sourcedir . '/Subs-Graphics.php');
3282
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3283
					{
3284
						$profile_vars['avatar'] = '';
3285
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3286
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3287
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3288
					}
3289
					else
3290
						return 'bad_avatar';
3291
				}
3292
			}
3293
		}
3294
	}
3295
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3296
	{
3297
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3298
		{
3299
			// Get the dimensions of the image.
3300
			if (!$downloadedExternalAvatar)
3301
			{
3302
				if (!is_writable($uploadDir))
3303
					fatal_lang_error('attachments_no_write', 'critical');
3304
3305
				$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3306
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3307
					fatal_lang_error('attach_timeout', 'critical');
3308
3309
				$_FILES['attachment']['tmp_name'] = $new_filename;
3310
			}
3311
3312
			$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3313
3314
			// No size, then it's probably not a valid pic.
3315
			if ($sizes === false)
3316
			{
3317
				@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3318
				return 'bad_avatar';
3319
			}
3320
			// Check whether the image is too large.
3321
			elseif ((!empty($modSettings['avatar_max_width_upload']) && $sizes[0] > $modSettings['avatar_max_width_upload']) || (!empty($modSettings['avatar_max_height_upload']) && $sizes[1] > $modSettings['avatar_max_height_upload']))
3322
			{
3323
				if (!empty($modSettings['avatar_resize_upload']))
3324
				{
3325
					// Attempt to chmod it.
3326
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3327
3328
					// @todo remove this require when appropriate
3329
					require_once($sourcedir . '/Subs-Graphics.php');
3330
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3331
					{
3332
						@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3333
						return 'bad_avatar';
3334
					}
3335
3336
					// Reset attachment avatar data.
3337
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3338
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3339
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3340
				}
3341
3342
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3343
				else
3344
				{
3345
					@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3346
					return 'bad_avatar_too_large';
3347
				}
3348
			}
3349
3350
			// So far, so good, checks lies ahead!
3351
			elseif (is_array($sizes))
3352
			{
3353
				// Now try to find an infection.
3354
				require_once($sourcedir . '/Subs-Graphics.php');
3355
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3356
				{
3357
					// It's bad. Try to re-encode the contents?
3358
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3359
					{
3360
						@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3361
						return 'bad_avatar_fail_reencode';
3362
					}
3363
					// We were successful. However, at what price?
3364
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3365
					// Hard to believe this would happen, but can you bet?
3366
					if ($sizes === false)
3367
					{
3368
						@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3369
						return 'bad_avatar';
3370
					}
3371
				}
3372
3373
				$extensions = array(
3374
					'1' => 'gif',
3375
					'2' => 'jpg',
3376
					'3' => 'png',
3377
					'6' => 'bmp'
3378
				);
3379
3380
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3381
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3382
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3383
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3384
				$file_hash = '';
3385
3386
				// Remove previous attachments this member might have had.
3387
				removeAttachments(array('id_member' => $memID));
3388
3389
				$cur_profile['id_attach'] = $smcFunc['db_insert']('',
3390
					'{db_prefix}attachments',
3391
					array(
3392
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3393
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3394
					),
3395
					array(
3396
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3397
						(int) $width, (int) $height, $mime_type, $id_folder,
3398
					),
3399
					array('id_attach'),
3400
					1
3401
				);
3402
3403
				$cur_profile['filename'] = $destName;
3404
				$cur_profile['attachment_type'] = 1;
3405
3406
				$destinationPath = $uploadDir . '/' . (empty($file_hash) ? $destName : $cur_profile['id_attach'] . '_' . $file_hash . '.dat');
3407
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3408
				{
3409
					// I guess a man can try.
3410
					removeAttachments(array('id_member' => $memID));
3411
					fatal_lang_error('attach_timeout', 'critical');
3412
				}
3413
3414
				// Attempt to chmod it.
3415
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3416
			}
3417
			$profile_vars['avatar'] = '';
3418
3419
			// Delete any temporary file.
3420
			if (file_exists($_FILES['attachment']['tmp_name']))
3421
				@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3422
		}
3423
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3424
		else
3425
			$profile_vars['avatar'] = '';
3426
	}
3427
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3428
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3429
	else
3430
		$profile_vars['avatar'] = '';
3431
3432
	// Setup the profile variables so it shows things right on display!
3433
	$cur_profile['avatar'] = $profile_vars['avatar'];
3434
3435
	return false;
3436
}
3437
3438
/**
3439
 * Validate the signature
3440
 *
3441
 * @param string &$value The new signature
3442
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3443
 */
3444
function profileValidateSignature(&$value)
3445
{
3446
	global $sourcedir, $modSettings, $smcFunc, $txt;
3447
3448
	require_once($sourcedir . '/Subs-Post.php');
3449
3450
	// Admins can do whatever they hell they want!
3451
	if (!allowedTo('admin_forum'))
3452
	{
3453
		// Load all the signature limits.
3454
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3455
		$sig_limits = explode(',', $sig_limits);
3456
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3457
3458
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3459
3460
		// Too many lines?
3461
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3462
		{
3463
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3464
			return 'signature_max_lines';
3465
		}
3466
3467
		// Too many images?!
3468 View Code Duplication
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
3469
		{
3470
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3471
			return 'signature_max_image_count';
3472
		}
3473
3474
		// What about too many smileys!
3475
		$smiley_parsed = $unparsed_signature;
3476
		parsesmileys($smiley_parsed);
3477
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3478
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3479
			return 'signature_allow_smileys';
3480
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3481
		{
3482
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3483
			return 'signature_max_smileys';
3484
		}
3485
3486
		// Maybe we are abusing font sizes?
3487
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3488
		{
3489
			foreach ($matches[1] as $ind => $size)
3490
			{
3491
				$limit_broke = 0;
3492
				// Attempt to allow all sizes of abuse, so to speak.
3493 View Code Duplication
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
3494
					$limit_broke = $sig_limits[7] . 'px';
3495
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3496
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3497
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3498
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3499
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3500
					$limit_broke = 'large';
3501
3502
				if ($limit_broke)
3503
				{
3504
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3505
					return 'signature_max_font_size';
3506
				}
3507
			}
3508
		}
3509
3510
		// The difficult one - image sizes! Don't error on this - just fix it.
3511
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3512
		{
3513
			// Get all BBC tags...
3514
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3515
			// ... and all HTML ones.
3516
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?' . '>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3517
			// And stick the HTML in the BBC.
3518 View Code Duplication
			if (!empty($matches2))
3519
			{
3520
				foreach ($matches2[0] as $ind => $dummy)
3521
				{
3522
					$matches[0][] = $matches2[0][$ind];
3523
					$matches[1][] = '';
3524
					$matches[2][] = '';
3525
					$matches[3][] = '';
3526
					$matches[4][] = '';
3527
					$matches[5][] = '';
3528
					$matches[6][] = '';
3529
					$matches[7][] = $matches2[1][$ind];
3530
				}
3531
			}
3532
3533
			$replaces = array();
3534
			// Try to find all the images!
3535
			if (!empty($matches))
3536
			{
3537
				foreach ($matches[0] as $key => $image)
3538
				{
3539
					$width = -1; $height = -1;
3540
3541
					// Does it have predefined restraints? Width first.
3542 View Code Duplication
					if ($matches[6][$key])
3543
						$matches[2][$key] = $matches[6][$key];
3544 View Code Duplication
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
3545
					{
3546
						$width = $sig_limits[5];
3547
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3548
					}
3549
					elseif ($matches[2][$key])
3550
						$width = $matches[2][$key];
3551
					// ... and height.
3552 View Code Duplication
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
3553
					{
3554
						$height = $sig_limits[6];
3555
						if ($width != -1)
3556
							$width = $width * ($height / $matches[4][$key]);
3557
					}
3558
					elseif ($matches[4][$key])
3559
						$height = $matches[4][$key];
3560
3561
					// If the dimensions are still not fixed - we need to check the actual image.
3562 View Code Duplication
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
3563
					{
3564
						$sizes = url_image_size($matches[7][$key]);
3565
						if (is_array($sizes))
3566
						{
3567
							// Too wide?
3568
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3569
							{
3570
								$width = $sig_limits[5];
3571
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3572
							}
3573
							// Too high?
3574
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3575
							{
3576
								$height = $sig_limits[6];
3577
								if ($width == -1)
3578
									$width = $sizes[0];
3579
								$width = $width * ($height / $sizes[1]);
3580
							}
3581
							elseif ($width != -1)
3582
								$height = $sizes[1];
3583
						}
3584
					}
3585
3586
					// Did we come up with some changes? If so remake the string.
3587 View Code Duplication
					if ($width != -1 || $height != -1)
3588
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3589
				}
3590
				if (!empty($replaces))
3591
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3592
			}
3593
		}
3594
3595
		// Any disabled BBC?
3596
		$disabledSigBBC = implode('|', $disabledTags);
3597
		if (!empty($disabledSigBBC))
3598
		{
3599
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3600
			{
3601
				$disabledTags = array_unique($disabledTags);
3602
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3603
				return 'signature_disabled_bbc';
3604
			}
3605
		}
3606
	}
3607
3608
	preparsecode($value);
3609
3610
	// Too long?
3611
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3612
	{
3613
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3614
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3615
		return 'signature_max_length';
3616
	}
3617
3618
	return true;
3619
}
3620
3621
/**
3622
 * Validate an email address.
3623
 *
3624
 * @param string $email The email address to validate
3625
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3626
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3627
 */
3628
function profileValidateEmail($email, $memID = 0)
3629
{
3630
	global $smcFunc;
3631
3632
	$email = strtr($email, array('&#039;' => '\''));
3633
3634
	// Check the name and email for validity.
3635
	if (trim($email) == '')
3636
		return 'no_email';
3637
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3638
		return 'bad_email';
3639
3640
	// Email addresses should be and stay unique.
3641
	$request = $smcFunc['db_query']('', '
3642
		SELECT id_member
3643
		FROM {db_prefix}members
3644
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3645
			email_address = {string:email_address}
3646
		LIMIT 1',
3647
		array(
3648
			'selected_member' => $memID,
3649
			'email_address' => $email,
3650
		)
3651
	);
3652
3653
	if ($smcFunc['db_num_rows']($request) > 0)
3654
		return 'email_taken';
3655
	$smcFunc['db_free_result']($request);
3656
3657
	return true;
3658
}
3659
3660
/**
3661
 * Reload a user's settings.
3662
 */
3663
function profileReloadUser()
3664
{
3665
	global $modSettings, $context, $cur_profile;
3666
3667
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3668
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3669
3670
	loadUserSettings();
3671
	writeLog();
3672
}
3673
3674
/**
3675
 * Send the user a new activation email if they need to reactivate!
3676
 */
3677
function profileSendActivation()
3678
{
3679
	global $sourcedir, $profile_vars, $context, $scripturl, $smcFunc, $cookiename, $cur_profile, $language, $modSettings;
3680
3681
	require_once($sourcedir . '/Subs-Post.php');
3682
3683
	// Shouldn't happen but just in case.
3684
	if (empty($profile_vars['email_address']))
3685
		return;
3686
3687
	$replacements = array(
3688
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3689
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3690
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3691
	);
3692
3693
	// Send off the email.
3694
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3695
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3696
3697
	// Log the user out.
3698
	$smcFunc['db_query']('', '
3699
		DELETE FROM {db_prefix}log_online
3700
		WHERE id_member = {int:selected_member}',
3701
		array(
3702
			'selected_member' => $context['id_member'],
3703
		)
3704
	);
3705
	$_SESSION['log_time'] = 0;
3706
	$_SESSION['login_' . $cookiename] = $smcFunc['json_encode'](array(0, '', 0));
3707
3708
	if (isset($_COOKIE[$cookiename]))
3709
		$_COOKIE[$cookiename] = '';
3710
3711
	loadUserSettings();
3712
3713
	$context['user']['is_logged'] = false;
3714
	$context['user']['is_guest'] = true;
3715
3716
	redirectexit('action=sendactivation');
3717
}
3718
3719
/**
3720
 * Function to allow the user to choose group membership etc...
3721
 *
3722
 * @param int $memID The ID of the member
3723
 */
3724
function groupMembership($memID)
3725
{
3726
	global $txt, $user_profile, $context, $smcFunc;
3727
3728
	$curMember = $user_profile[$memID];
3729
	$context['primary_group'] = $curMember['id_group'];
3730
3731
	// Can they manage groups?
3732
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3733
	$context['can_manage_protected'] = allowedTo('admin_forum');
3734
	$context['can_edit_primary'] = $context['can_manage_protected'];
3735
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
3736
3737
	// Get all the groups this user is a member of.
3738
	$groups = explode(',', $curMember['additional_groups']);
3739
	$groups[] = $curMember['id_group'];
3740
3741
	// Ensure the query doesn't croak!
3742
	if (empty($groups))
3743
		$groups = array(0);
3744
	// Just to be sure...
3745
	foreach ($groups as $k => $v)
3746
		$groups[$k] = (int) $v;
3747
3748
	// Get all the membergroups they can join.
3749
	$request = $smcFunc['db_query']('', '
3750
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
3751
			COALESCE(lgr.id_member, 0) AS pending
3752
		FROM {db_prefix}membergroups AS mg
3753
			LEFT JOIN {db_prefix}log_group_requests AS lgr ON (lgr.id_member = {int:selected_member} AND lgr.id_group = mg.id_group AND lgr.status = {int:status_open})
3754
		WHERE (mg.id_group IN ({array_int:group_list})
3755
			OR mg.group_type > {int:nonjoin_group_id})
3756
			AND mg.min_posts = {int:min_posts}
3757
			AND mg.id_group != {int:moderator_group}
3758
		ORDER BY group_name',
3759
		array(
3760
			'group_list' => $groups,
3761
			'selected_member' => $memID,
3762
			'status_open' => 0,
3763
			'nonjoin_group_id' => 1,
3764
			'min_posts' => -1,
3765
			'moderator_group' => 3,
3766
		)
3767
	);
3768
	// This beast will be our group holder.
3769
	$context['groups'] = array(
3770
		'member' => array(),
3771
		'available' => array()
3772
	);
3773
	while ($row = $smcFunc['db_fetch_assoc']($request))
3774
	{
3775
		// Can they edit their primary group?
3776
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
3777
			$context['can_edit_primary'] = true;
3778
3779
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
3780
		if (((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0)) && $row['id_group'] != $context['primary_group'])
3781
			continue;
3782
3783
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
3784
			'id' => $row['id_group'],
3785
			'name' => $row['group_name'],
3786
			'desc' => $row['description'],
3787
			'color' => $row['online_color'],
3788
			'type' => $row['group_type'],
3789
			'pending' => $row['pending'],
3790
			'is_primary' => $row['id_group'] == $context['primary_group'],
3791
			'can_be_primary' => $row['hidden'] != 2,
3792
			// Anything more than this needs to be done through account settings for security.
3793
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
3794
		);
3795
	}
3796
	$smcFunc['db_free_result']($request);
3797
3798
	// Add registered members on the end.
3799
	$context['groups']['member'][0] = array(
3800
		'id' => 0,
3801
		'name' => $txt['regular_members'],
3802
		'desc' => $txt['regular_members_desc'],
3803
		'type' => 0,
3804
		'is_primary' => $context['primary_group'] == 0 ? true : false,
3805
		'can_be_primary' => true,
3806
		'can_leave' => 0,
3807
	);
3808
3809
	// No changing primary one unless you have enough groups!
3810
	if (count($context['groups']['member']) < 2)
3811
		$context['can_edit_primary'] = false;
3812
3813
	// In the special case that someone is requesting membership of a group, setup some special context vars.
3814
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
3815
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
3816
}
3817
3818
/**
3819
 * This function actually makes all the group changes
3820
 *
3821
 * @param array $profile_vars The profile variables
3822
 * @param array $post_errors Any errors that have occurred
3823
 * @param int $memID The ID of the member
3824
 * @return string What type of change this is - 'primary' if changing the primary group, 'request' if requesting to join a group or 'free' if it's an open group
3825
 */
3826
function groupMembership2($profile_vars, $post_errors, $memID)
0 ignored issues
show
Unused Code introduced by
The parameter $profile_vars is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $post_errors is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3827
{
3828
	global $user_info, $context, $user_profile, $modSettings, $smcFunc;
3829
3830
	// Let's be extra cautious...
3831
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
3832
		isAllowedTo('manage_membergroups');
3833
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
3834
		fatal_lang_error('no_access', false);
3835
3836
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
3837
3838
	$old_profile = &$user_profile[$memID];
3839
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3840
	$context['can_manage_protected'] = allowedTo('admin_forum');
3841
3842
	// By default the new primary is the old one.
3843
	$newPrimary = $old_profile['id_group'];
3844
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
3845
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
3846
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
3847
3848
	// One way or another, we have a target group in mind...
3849
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
3850
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
3851
3852
	// Sanity check!!
3853
	if ($group_id == 1)
3854
		isAllowedTo('admin_forum');
3855
	// Protected groups too!
3856
	else
3857
	{
3858
		$request = $smcFunc['db_query']('', '
3859
			SELECT group_type
3860
			FROM {db_prefix}membergroups
3861
			WHERE id_group = {int:current_group}
3862
			LIMIT {int:limit}',
3863
			array(
3864
				'current_group' => $group_id,
3865
				'limit' => 1,
3866
			)
3867
		);
3868
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
3869
		$smcFunc['db_free_result']($request);
3870
3871
		if ($is_protected == 1)
3872
			isAllowedTo('admin_forum');
3873
	}
3874
3875
	// What ever we are doing, we need to determine if changing primary is possible!
3876
	$request = $smcFunc['db_query']('', '
3877
		SELECT id_group, group_type, hidden, group_name
3878
		FROM {db_prefix}membergroups
3879
		WHERE id_group IN ({int:group_list}, {int:current_group})',
3880
		array(
3881
			'group_list' => $group_id,
3882
			'current_group' => $old_profile['id_group'],
3883
		)
3884
	);
3885
	while ($row = $smcFunc['db_fetch_assoc']($request))
3886
	{
3887
		// Is this the new group?
3888
		if ($row['id_group'] == $group_id)
3889
		{
3890
			$foundTarget = true;
3891
			$group_name = $row['group_name'];
3892
3893
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
3894
			if ($changeType == 'request' && $row['group_type'] != 2)
3895
				fatal_lang_error('no_access', false);
3896
			// What about leaving a requestable group we are not a member of?
3897
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
3898
				fatal_lang_error('no_access', false);
3899
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
3900
				fatal_lang_error('no_access', false);
3901
3902
			// We can't change the primary group if this is hidden!
3903
			if ($row['hidden'] == 2)
3904
				$canChangePrimary = false;
3905
		}
3906
3907
		// If this is their old primary, can we change it?
3908 View Code Duplication
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
3909
			$canChangePrimary = 1;
3910
3911
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
3912
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
3913
			$canChangePrimary = false;
3914
3915
		// If this is the one we are acting on, can we even act?
3916 View Code Duplication
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
3917
			$canChangePrimary = false;
3918
	}
3919
	$smcFunc['db_free_result']($request);
3920
3921
	// Didn't find the target?
3922
	if (!$foundTarget)
3923
		fatal_lang_error('no_access', false);
3924
3925
	// Final security check, don't allow users to promote themselves to admin.
3926
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
3927
	{
3928
		$request = $smcFunc['db_query']('', '
3929
			SELECT COUNT(permission)
3930
			FROM {db_prefix}permissions
3931
			WHERE id_group = {int:selected_group}
3932
				AND permission = {string:admin_forum}
3933
				AND add_deny = {int:not_denied}',
3934
			array(
3935
				'selected_group' => $group_id,
3936
				'not_denied' => 1,
3937
				'admin_forum' => 'admin_forum',
3938
			)
3939
		);
3940
		list ($disallow) = $smcFunc['db_fetch_row']($request);
3941
		$smcFunc['db_free_result']($request);
3942
3943
		if ($disallow)
3944
			isAllowedTo('admin_forum');
3945
	}
3946
3947
	// If we're requesting, add the note then return.
3948
	if ($changeType == 'request')
3949
	{
3950
		$request = $smcFunc['db_query']('', '
3951
			SELECT id_member
3952
			FROM {db_prefix}log_group_requests
3953
			WHERE id_member = {int:selected_member}
3954
				AND id_group = {int:selected_group}
3955
				AND status = {int:status_open}',
3956
			array(
3957
				'selected_member' => $memID,
3958
				'selected_group' => $group_id,
3959
				'status_open' => 0,
3960
			)
3961
		);
3962
		if ($smcFunc['db_num_rows']($request) != 0)
3963
			fatal_lang_error('profile_error_already_requested_group');
3964
		$smcFunc['db_free_result']($request);
3965
3966
		// Log the request.
3967
		$smcFunc['db_insert']('',
3968
			'{db_prefix}log_group_requests',
3969
			array(
3970
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
3971
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
3972
			),
3973
			array(
3974
				$memID, $group_id, time(), $_POST['reason'],
3975
				0, 0, '', 0, '',
3976
			),
3977
			array('id_request')
3978
		);
3979
3980
		// Set up some data for our background task...
3981
		$data = $smcFunc['json_encode'](array('id_member' => $memID, 'member_name' => $user_info['name'], 'id_group' => $group_id, 'group_name' => $group_name, 'reason' => $_POST['reason'], 'time' => time()));
0 ignored issues
show
Bug introduced by
The variable $group_name does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3982
3983
		// Add a background task to handle notifying people of this request
3984
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
3985
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
3986
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
3987
		);
3988
3989
		return $changeType;
3990
	}
3991
	// Otherwise we are leaving/joining a group.
3992
	elseif ($changeType == 'free')
3993
	{
3994
		// Are we leaving?
3995
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
3996
		{
3997
			if ($old_profile['id_group'] == $group_id)
3998
				$newPrimary = 0;
3999
			else
4000
				unset($addGroups[$group_id]);
4001
		}
4002
		// ... if not, must be joining.
4003
		else
4004
		{
4005
			// Can we change the primary, and do we want to?
4006
			if ($canChangePrimary)
0 ignored issues
show
Bug Best Practice introduced by
The expression $canChangePrimary of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
4007
			{
4008
				if ($old_profile['id_group'] != 0)
4009
					$addGroups[$old_profile['id_group']] = -1;
4010
				$newPrimary = $group_id;
4011
			}
4012
			// Otherwise it's an additional group...
4013
			else
4014
				$addGroups[$group_id] = -1;
4015
		}
4016
	}
4017
	// Finally, we must be setting the primary.
4018
	elseif ($canChangePrimary)
0 ignored issues
show
Bug Best Practice introduced by
The expression $canChangePrimary of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
4019
	{
4020
		if ($old_profile['id_group'] != 0)
4021
			$addGroups[$old_profile['id_group']] = -1;
4022
		if (isset($addGroups[$group_id]))
4023
			unset($addGroups[$group_id]);
4024
		$newPrimary = $group_id;
4025
	}
4026
4027
	// Finally, we can make the changes!
4028
	foreach ($addGroups as $id => $dummy)
4029
		if (empty($id))
4030
			unset($addGroups[$id]);
4031
	$addGroups = implode(',', array_flip($addGroups));
4032
4033
	// Ensure that we don't cache permissions if the group is changing.
4034 View Code Duplication
	if ($context['user']['is_owner'])
4035
		$_SESSION['mc']['time'] = 0;
4036
	else
4037
		updateSettings(array('settings_updated' => time()));
4038
4039
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
4040
4041
	return $changeType;
4042
}
4043
4044
/**
4045
 * Provides interface to setup Two Factor Auth in SMF
4046
 *
4047
 * @param int $memID The ID of the member
4048
 */
4049
function tfasetup($memID)
4050
{
4051
	global $user_info, $context, $user_settings, $sourcedir, $modSettings;
4052
4053
	require_once($sourcedir . '/Class-TOTP.php');
4054
	require_once($sourcedir . '/Subs-Auth.php');
4055
4056
	// If TFA has not been setup, allow them to set it up
4057
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4058
	{
4059
		// Check to ensure we're forcing SSL for authentication
4060 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
0 ignored issues
show
Bug introduced by
The variable $maintenance seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
4061
			fatal_lang_error('login_ssl_required');
4062
4063
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4064
		// we do not want too much AJAX to confuse them.
4065
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4066
		{
4067
			$context['from_ajax'] = true;
4068
			$context['template_layers'] = array();
4069
		}
4070
4071
		// When the code is being sent, verify to make sure the user got it right
4072
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4073
		{
4074
			$code = $_POST['tfa_code'];
4075
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4076
			$totp->setRange(1);
4077
			$valid_password = hash_verify_password($user_settings['member_name'], trim($_POST['passwd']), $user_settings['passwd']);
4078
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4079
4080
			if ($valid_password && $valid_code)
4081
			{
4082
				$backup = substr(sha1(mt_rand()), 0, 16);
4083
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4084
4085
				updateMemberData($memID, array(
4086
					'tfa_secret' => $_SESSION['tfa_secret'],
4087
					'tfa_backup' => $backup_encrypted,
4088
				));
4089
4090
				setTFACookie(3153600, $memID, hash_salt($backup_encrypted, $user_settings['password_salt']));
0 ignored issues
show
Bug introduced by
It seems like $backup_encrypted defined by hash_password($user_sett...member_name'], $backup) on line 4083 can also be of type false or null; however, hash_salt() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4091
4092
				unset($_SESSION['tfa_secret']);
4093
4094
				$context['tfa_backup'] = $backup;
4095
				$context['sub_template'] = 'tfasetup_backup';
4096
4097
				return;
4098
			}
4099
			else
4100
			{
4101
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4102
				$context['tfa_error'] = !$valid_code;
4103
				$context['tfa_pass_error'] = !$valid_password;
4104
				$context['tfa_pass_value'] = $_POST['passwd'];
4105
				$context['tfa_value'] = $_POST['tfa_code'];
4106
			}
4107
		}
4108
		else
4109
		{
4110
			$totp = new \TOTP\Auth();
4111
			$secret = $totp->generateCode();
4112
			$_SESSION['tfa_secret'] = $secret;
4113
			$context['tfa_secret'] = $secret;
4114
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4115
		}
4116
4117
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4118
	}
4119
	else
4120
		redirectexit('action=profile;area=account;u=' . $memID);
4121
}
4122
4123
/**
4124
 * Provides interface to disable two-factor authentication in SMF
4125
 *
4126
 * @param int $memID The ID of the member
4127
 */
4128
function tfadisable($memID)
4129
{
4130
	global $context, $modSettings, $smcFunc, $user_settings;
4131
4132
	if (!empty($user_settings['tfa_secret']))
4133
	{
4134
		// Bail if we're forcing SSL for authentication and the network connection isn't secure.
4135
		if (!empty($modSettings['force_ssl']) && !httpsOn())
4136
			fatal_lang_error('login_ssl_required', false);
4137
4138
		// The admin giveth...
4139
		elseif ($modSettings['tfa_mode'] == 3 && $context['user']['is_owner'])
4140
			fatal_lang_error('cannot_disable_tfa', false);
4141
		elseif ($modSettings['tfa_mode'] == 2 && $context['user']['is_owner'])
4142
		{
4143
			$groups = array($user_settings['id_group']);
4144 View Code Duplication
			if (!empty($user_settings['additional_groups']))
4145
				$groups = array_unique(array_merge($groups, explode(',', $user_settings['additional_groups'])));
4146
4147
			$request = $smcFunc['db_query']('', '
4148
				SELECT id_group
4149
				FROM {db_prefix}membergroups
4150
				WHERE tfa_required = {int:tfa_required}
4151
					AND id_group IN ({array_int:groups})',
4152
				array(
4153
					'tfa_required' => 1,
4154
					'groups' => $groups,
4155
				)
4156
			);
4157
			// They belong to a membergroup that requires tfa.
4158
			if (!empty($smcFunc['db_num_rows']($request)))
4159
				fatal_lang_error('cannot_disable_tfa2', false);
4160
			$smcFunc['db_free_result']($request);
4161
		}
4162
	}
4163
	else
4164
		redirectexit('action=profile;area=account;u=' . $memID);
4165
}
4166
4167
?>