Completed
Push — release-2.1 ( 99ca30...c081b8 )
by Michael
07:09
created

Profile-Modify.php ➔ alert_count()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 2
nop 2
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file has the primary job of showing and editing people's profiles.
5
 * 	It also allows the user to change some of their or another's preferences,
6
 * 	and such things
7
 *
8
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines http://www.simplemachines.org
12
 * @copyright 2017 Simple Machines and individual contributors
13
 * @license http://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1 Beta 3
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'] == '0001-01-01' ? '0000-00-00' : $cur_profile['birthdate']);
93
				$context['member']['birth_date'] = array(
94
					'year' => $uyear == '0004' ? '0000' : $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 = '0001-01-01';
108
					else
109
						$value = checkdate($value, $_POST['bday2'], $_POST['bday3'] < 4 ? 4 : $_POST['bday3']) ? sprintf('%04d-%02d-%02d', $_POST['bday3'] < 4 ? 4 : $_POST['bday3'], $_POST['bday1'], $_POST['bday2']) : '0001-01-01';
110
				}
111
				else
112
					$value = '0001-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]) : '0001-01-01';
129
					return true;
130
				}
131
				else
132
				{
133
					$value = empty($cur_profile['birthdate']) ? '0001-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 != "'. $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)
437
			{
438
				$value = $value != '' ? md5($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://'))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
590
					$value = '';
591
				return true;
592
			},
593
			'link_with' => 'website',
594
		),
595
	);
596
597
	call_integration_hook('integrate_load_profile_fields', array(&$profile_fields));
598
599
	$disabled_fields = !empty($modSettings['disabled_profile_fields']) ? explode(',', $modSettings['disabled_profile_fields']) : array();
600
	// For each of the above let's take out the bits which don't apply - to save memory and security!
601
	foreach ($profile_fields as $key => $field)
602
	{
603
		// Do we have permission to do this?
604
		if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? array($field['permission'] . '_own', $field['permission'] . '_any') : $field['permission'] . '_any')) && !allowedTo($field['permission']))
605
			unset($profile_fields[$key]);
606
607
		// Is it enabled?
608
		if (isset($field['enabled']) && !$field['enabled'])
609
			unset($profile_fields[$key]);
610
611
		// Is it specifically disabled?
612
		if (in_array($key, $disabled_fields) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields)))
613
			unset($profile_fields[$key]);
614
	}
615
}
616
617
/**
618
 * Setup the context for a page load!
619
 *
620
 * @param array $fields The profile fields to display. Each item should correspond to an item in the $profile_fields array generated by loadProfileFields
621
 */
622
function setupProfileContext($fields)
623
{
624
	global $profile_fields, $context, $cur_profile, $txt;
625
626
	// Some default bits.
627
	$context['profile_prehtml'] = '';
628
	$context['profile_posthtml'] = '';
629
	$context['profile_javascript'] = '';
630
	$context['profile_onsubmit_javascript'] = '';
631
632
	call_integration_hook('integrate_setup_profile_context', array(&$fields));
633
634
	// Make sure we have this!
635
	loadProfileFields(true);
636
637
	// First check for any linked sets.
638
	foreach ($profile_fields as $key => $field)
639
		if (isset($field['link_with']) && in_array($field['link_with'], $fields))
640
			$fields[] = $key;
641
642
	$i = 0;
643
	$last_type = '';
644
	foreach ($fields as $key => $field)
645
	{
646
		if (isset($profile_fields[$field]))
647
		{
648
			// Shortcut.
649
			$cur_field = &$profile_fields[$field];
650
651
			// Does it have a preload and does that preload succeed?
652
			if (isset($cur_field['preload']) && !$cur_field['preload']())
653
				continue;
654
655
			// If this is anything but complex we need to do more cleaning!
656
			if ($cur_field['type'] != 'callback' && $cur_field['type'] != 'hidden')
657
			{
658
				if (!isset($cur_field['label']))
659
					$cur_field['label'] = isset($txt[$field]) ? $txt[$field] : $field;
660
661
				// Everything has a value!
662
				if (!isset($cur_field['value']))
663
					$cur_field['value'] = isset($cur_profile[$field]) ? $cur_profile[$field] : '';
664
665
				// Any input attributes?
666
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
667
			}
668
669
			// Was there an error with this field on posting?
670
			if (isset($context['profile_errors'][$field]))
671
				$cur_field['is_error'] = true;
672
673
			// Any javascript stuff?
674
			if (!empty($cur_field['js_submit']))
675
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
676
			if (!empty($cur_field['js']))
677
				$context['profile_javascript'] .= $cur_field['js'];
678
679
			// Any template stuff?
680
			if (!empty($cur_field['prehtml']))
681
				$context['profile_prehtml'] .= $cur_field['prehtml'];
682
			if (!empty($cur_field['posthtml']))
683
				$context['profile_posthtml'] .= $cur_field['posthtml'];
684
685
			// Finally put it into context?
686
			if ($cur_field['type'] != 'hidden')
687
			{
688
				$last_type = $cur_field['type'];
689
				$context['profile_fields'][$field] = &$profile_fields[$field];
690
			}
691
		}
692
		// Bodge in a line break - without doing two in a row ;)
693
		elseif ($field == 'hr' && $last_type != 'hr' && $last_type != '')
694
		{
695
			$last_type = 'hr';
696
			$context['profile_fields'][$i++]['type'] = 'hr';
697
		}
698
	}
699
700
	// Some spicy JS.
701
	addInlineJavaScript('
702
	var form_handle = document.forms.creator;
703
	createEventListener(form_handle);
704
	'. (!empty($context['require_password']) ? '
705
	form_handle.addEventListener(\'submit\', function(event)
706
	{
707
		if (this.oldpasswrd.value == "")
708
		{
709
			event.preventDefault();
710
			alert('. (JavaScriptEscape($txt['required_security_reasons'])) . ');
711
			return false;
712
		}
713
	}, false);' : ''), true);
714
715
	// Any onsubmit javascript?
716
	if (!empty($context['profile_onsubmit_javascript']))
717
		addInlineJavaScript($context['profile_onsubmit_javascript'], true);
718
719
	// Any totally custom stuff?
720
	if (!empty($context['profile_javascript']))
721
		addInlineJavaScript($context['profile_javascript'], true);
722
723
	// Free up some memory.
724
	unset($profile_fields);
725
}
726
727
/**
728
 * Save the profile changes.
729
 */
730
function saveProfileFields()
731
{
732
	global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile;
733
734
	// Load them up.
735
	loadProfileFields();
736
737
	// This makes things easier...
738
	$old_profile = $cur_profile;
739
740
	// This allows variables to call activities when they save - by default just to reload their settings
741
	$context['profile_execute_on_save'] = array();
742
	if ($context['user']['is_owner'])
743
		$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
744
745
	// Assume we log nothing.
746
	$context['log_changes'] = array();
747
748
	// Cycle through the profile fields working out what to do!
749
	foreach ($profile_fields as $key => $field)
750
	{
751
		if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key == 'signature'))
752
			continue;
753
754
		// What gets updated?
755
		$db_key = isset($field['save_key']) ? $field['save_key'] : $key;
756
757
		// Right - we have something that is enabled, we can act upon and has a value posted to it. Does it have a validation function?
758
		if (isset($field['input_validate']))
759
		{
760
			$is_valid = $field['input_validate']($_POST[$key]);
761
			// An error occurred - set it as such!
762
			if ($is_valid !== true)
763
			{
764
				// Is this an actual error?
765
				if ($is_valid !== false)
766
				{
767
					$post_errors[$key] = $is_valid;
768
					$profile_fields[$key]['is_error'] = $is_valid;
769
				}
770
				// Retain the old value.
771
				$cur_profile[$key] = $_POST[$key];
772
				continue;
773
			}
774
		}
775
776
		// Are we doing a cast?
777
		$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
778
779
		// Finally, clean up certain types.
780
		if ($field['cast_type'] == 'int')
781
			$_POST[$key] = (int) $_POST[$key];
782
		elseif ($field['cast_type'] == 'float')
783
			$_POST[$key] = (float) $_POST[$key];
784
		elseif ($field['cast_type'] == 'check')
785
			$_POST[$key] = !empty($_POST[$key]) ? 1 : 0;
786
787
		// If we got here we're doing OK.
788
		if ($field['type'] != 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
789
		{
790
			// Set the save variable.
791
			$profile_vars[$db_key] = $_POST[$key];
792
			// And update the user profile.
793
			$cur_profile[$key] = $_POST[$key];
794
795
			// Are we logging it?
796
			if (!empty($field['log_change']) && isset($old_profile[$key]))
797
				$context['log_changes'][$key] = array(
798
					'previous' => $old_profile[$key],
799
					'new' => $_POST[$key],
800
				);
801
		}
802
803
		// Logging group changes are a bit different...
804
		if ($key == 'id_group' && $field['log_change'])
805
		{
806
			profileLoadGroups();
807
808
			// Any changes to primary group?
809
			if ($_POST['id_group'] != $old_profile['id_group'])
810
			{
811
				$context['log_changes']['id_group'] = array(
812
					'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
813
					'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
814
				);
815
			}
816
817
			// Prepare additional groups for comparison.
818
			$additional_groups = array(
819
				'previous' => !empty($old_profile['additional_groups']) ? explode(',', $old_profile['additional_groups']) : array(),
820
				'new' => !empty($_POST['additional_groups']) ? array_diff($_POST['additional_groups'], array(0)) : array(),
821
			);
822
823
			sort($additional_groups['previous']);
824
			sort($additional_groups['new']);
825
826
			// What about additional groups?
827
			if ($additional_groups['previous'] != $additional_groups['new'])
828
			{
829
				foreach ($additional_groups as $type => $groups)
830
				{
831
					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...
832
					{
833
						if (isset($context['member_groups'][$group]))
834
							$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
835
						else
836
							unset($additional_groups[$type][$id]);
837
					}
838
					$additional_groups[$type] = implode(', ', $additional_groups[$type]);
839
				}
840
841
				$context['log_changes']['additional_groups'] = $additional_groups;
842
			}
843
		}
844
	}
845
846
	// @todo Temporary
847
	if ($context['user']['is_owner'])
848
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
849
	else
850
		$changeOther = allowedTo('profile_extra_any');
851
	if ($changeOther && empty($post_errors))
852
	{
853
		makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
854
		if (!empty($_REQUEST['sa']))
855
		{
856
			$custom_fields_errors = makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false, true);
857
858
			if (!empty($custom_fields_errors))
859
				$post_errors = array_merge($post_errors, $custom_fields_errors);
860
		}
861
	}
862
863
	// Free memory!
864
	unset($profile_fields);
865
}
866
867
/**
868
 * Save the profile changes
869
 *
870
 * @param array &$profile_vars The items to save
871
 * @param array &$post_errors An array of information about any errors that occurred
872
 * @param int $memID The ID of the member whose profile we're saving
873
 */
874
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...
875
{
876
	global $user_profile, $context;
877
878
	// These make life easier....
879
	$old_profile = &$user_profile[$memID];
880
881
	// Permissions...
882
	if ($context['user']['is_owner'])
883
	{
884
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own', 'profile_website_any', 'profile_website_own', 'profile_signature_any', 'profile_signature_own'));
885
	}
886
	else
887
		$changeOther = allowedTo(array('profile_extra_any', 'profile_website_any', 'profile_signature_any'));
888
889
	// Arrays of all the changes - makes things easier.
890
	$profile_bools = array();
891
	$profile_ints = array();
892
	$profile_floats = array();
893
	$profile_strings = array(
894
		'buddy_list',
895
		'ignore_boards',
896
	);
897
898
	if (isset($_POST['sa']) && $_POST['sa'] == 'ignoreboards' && empty($_POST['ignore_brd']))
899
		$_POST['ignore_brd'] = array();
900
901
	unset($_POST['ignore_boards']); // Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
902
	if (isset($_POST['ignore_brd']))
903
	{
904
		if (!is_array($_POST['ignore_brd']))
905
			$_POST['ignore_brd'] = array($_POST['ignore_brd']);
906
907
		foreach ($_POST['ignore_brd'] as $k => $d)
908
		{
909
			$d = (int) $d;
910
			if ($d != 0)
911
				$_POST['ignore_brd'][$k] = $d;
912
			else
913
				unset($_POST['ignore_brd'][$k]);
914
		}
915
		$_POST['ignore_boards'] = implode(',', $_POST['ignore_brd']);
916
		unset($_POST['ignore_brd']);
917
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);
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
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))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
	$log_changes = array();
1171
	while ($row = $smcFunc['db_fetch_assoc']($request))
1172
	{
1173
		/* This means don't save if:
1174
			- The user is NOT an admin.
1175
			- The data is not freely viewable and editable by users.
1176
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
1177
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
1178
		*/
1179
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != $user_info['id'] || $row['private'] != 2) && ($area != 'register' || $row['show_reg'] == 0))
1180
			continue;
1181
1182
		// Validate the user data.
1183
		if ($row['field_type'] == 'check')
1184
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
1185
		elseif ($row['field_type'] == 'select' || $row['field_type'] == 'radio')
1186
		{
1187
			$value = $row['default_value'];
1188
			foreach (explode(',', $row['field_options']) as $k => $v)
1189
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
1190
					$value = $v;
1191
		}
1192
		// Otherwise some form of text!
1193
		else
1194
		{
1195
			$value = isset($_POST['customfield'][$row['col_name']]) ? $_POST['customfield'][$row['col_name']] : '';
1196
			if ($row['field_length'])
1197
				$value = $smcFunc['substr']($value, 0, $row['field_length']);
1198
1199
			// Any masks?
1200
			if ($row['field_type'] == 'text' && !empty($row['mask']) && $row['mask'] != 'none')
1201
			{
1202
				if ($row['mask'] == 'email' && (!filter_var($value, FILTER_VALIDATE_EMAIL) || strlen($value) > 255))
1203
				{
1204
					if ($returnErrors)
1205
						$errors[] = 'custom_field_mail_fail';
1206
1207
					else
1208
						$value = '';
1209
				}
1210
				elseif ($row['mask'] == 'number')
1211
				{
1212
					$value = (int) $value;
1213
				}
1214
				elseif (substr($row['mask'], 0, 5) == 'regex' && trim($value) != '' && preg_match(substr($row['mask'], 5), $value) === 0)
1215
				{
1216
					if ($returnErrors)
1217
						$errors[] = 'custom_field_regex_fail';
1218
1219
					else
1220
						$value = '';
1221
				}
1222
			}
1223
		}
1224
1225
		// Did it change?
1226
		if (!isset($user_profile[$memID]['options'][$row['col_name']]) || $user_profile[$memID]['options'][$row['col_name']] !== $value)
1227
		{
1228
			$log_changes[] = array(
1229
				'action' => 'customfield_' . $row['col_name'],
1230
				'log_type' => 'user',
1231
				'extra' => array(
1232
					'previous' => !empty($user_profile[$memID]['options'][$row['col_name']]) ? $user_profile[$memID]['options'][$row['col_name']] : '',
1233
					'new' => $value,
1234
					'applicator' => $user_info['id'],
1235
					'member_affected' => $memID,
1236
				),
1237
			);
1238
			$changes[] = array(1, $row['col_name'], $value, $memID);
1239
			$user_profile[$memID]['options'][$row['col_name']] = $value;
1240
		}
1241
	}
1242
	$smcFunc['db_free_result']($request);
1243
1244
	$hook_errors = call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, &$errors, $returnErrors, $memID, $area, $sanitize));
1245
1246
	if (!empty($hook_errors) && is_array($hook_errors))
1247
		$errors = array_merge($errors, $hook_errors);
1248
1249
	// Make those changes!
1250
	if (!empty($changes) && empty($context['password_auth_failed']) && empty($errors))
1251
	{
1252
		$smcFunc['db_insert']('replace',
1253
			'{db_prefix}themes',
1254
			array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
1255
			$changes,
1256
			array('id_theme', 'variable', 'id_member')
1257
		);
1258
		if (!empty($log_changes) && !empty($modSettings['modlog_enabled']))
1259
		{
1260
			require_once($sourcedir . '/Logging.php');
1261
			logActions($log_changes);
1262
		}
1263
	}
1264
1265
	if ($returnErrors)
1266
		return $errors;
1267
}
1268
1269
/**
1270
 * Show all the users buddies, as well as a add/delete interface.
1271
 *
1272
 * @param int $memID The ID of the member
1273
 */
1274
function editBuddyIgnoreLists($memID)
1275
{
1276
	global $context, $txt, $modSettings;
1277
1278
	// Do a quick check to ensure people aren't getting here illegally!
1279
	if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
1280
		fatal_lang_error('no_access', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
1281
1282
	// Can we email the user direct?
1283
	$context['can_moderate_forum'] = allowedTo('moderate_forum');
1284
	$context['can_send_email'] = allowedTo('moderate_forum');
1285
1286
	$subActions = array(
1287
		'buddies' => array('editBuddies', $txt['editBuddies']),
1288
		'ignore' => array('editIgnoreList', $txt['editIgnoreList']),
1289
	);
1290
1291
	$context['list_area'] = isset($_GET['sa']) && isset($subActions[$_GET['sa']]) ? $_GET['sa'] : 'buddies';
1292
1293
	// Create the tabs for the template.
1294
	$context[$context['profile_menu_name']]['tab_data'] = array(
1295
		'title' => $txt['editBuddyIgnoreLists'],
1296
		'description' => $txt['buddy_ignore_desc'],
1297
		'icon' => 'profile_hd.png',
1298
		'tabs' => array(
1299
			'buddies' => array(),
1300
			'ignore' => array(),
1301
		),
1302
	);
1303
1304
	loadJavaScriptFile('suggest.js', array('defer' => false), 'smf_suggest');
1305
1306
	// Pass on to the actual function.
1307
	$context['sub_template'] = $subActions[$context['list_area']][0];
1308
	$call = call_helper($subActions[$context['list_area']][0], true);
1309
1310
	if (!empty($call))
1311
		call_user_func($call, $memID);
1312
}
1313
1314
/**
1315
 * Show all the users buddies, as well as a add/delete interface.
1316
 *
1317
 * @param int $memID The ID of the member
1318
 */
1319
function editBuddies($memID)
1320
{
1321
	global $txt, $scripturl, $settings;
1322
	global $context, $user_profile, $memberContext, $smcFunc;
1323
1324
	// For making changes!
1325
	$buddiesArray = explode(',', $user_profile[$memID]['buddy_list']);
1326
	foreach ($buddiesArray as $k => $dummy)
1327
		if ($dummy == '')
1328
			unset($buddiesArray[$k]);
1329
1330
	// Removing a buddy?
1331
	if (isset($_GET['remove']))
1332
	{
1333
		checkSession('get');
1334
1335
		call_integration_hook('integrate_remove_buddy', array($memID));
1336
1337
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1338
1339
		// Heh, I'm lazy, do it the easy way...
1340
		foreach ($buddiesArray as $key => $buddy)
1341
			if ($buddy == (int) $_GET['remove'])
1342
			{
1343
				unset($buddiesArray[$key]);
1344
				$_SESSION['prf-save'] = true;
1345
			}
1346
1347
		// Make the changes.
1348
		$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1349
		updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1350
1351
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1352
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1353
	}
1354
	elseif (isset($_POST['new_buddy']))
1355
	{
1356
		checkSession();
1357
1358
		// Prepare the string for extraction...
1359
		$_POST['new_buddy'] = strtr($smcFunc['htmlspecialchars']($_POST['new_buddy'], ENT_QUOTES), array('&quot;' => '"'));
1360
		preg_match_all('~"([^"]+)"~', $_POST['new_buddy'], $matches);
1361
		$new_buddies = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_buddy']))));
1362
1363 View Code Duplication
		foreach ($new_buddies as $k => $dummy)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1364
		{
1365
			$new_buddies[$k] = strtr(trim($new_buddies[$k]), array('\'' => '&#039;'));
1366
1367
			if (strlen($new_buddies[$k]) == 0 || in_array($new_buddies[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1368
				unset($new_buddies[$k]);
1369
		}
1370
1371
		call_integration_hook('integrate_add_buddies', array($memID, &$new_buddies));
1372
1373
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1374 View Code Duplication
		if (!empty($new_buddies))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1375
		{
1376
			// Now find out the id_member of the buddy.
1377
			$request = $smcFunc['db_query']('', '
1378
				SELECT id_member
1379
				FROM {db_prefix}members
1380
				WHERE member_name IN ({array_string:new_buddies}) OR real_name IN ({array_string:new_buddies})
1381
				LIMIT {int:count_new_buddies}',
1382
				array(
1383
					'new_buddies' => $new_buddies,
1384
					'count_new_buddies' => count($new_buddies),
1385
				)
1386
			);
1387
1388
			if ($smcFunc['db_num_rows']($request) != 0)
1389
				$_SESSION['prf-save'] = true;
1390
1391
			// Add the new member to the buddies array.
1392
			while ($row = $smcFunc['db_fetch_assoc']($request))
1393
			{
1394
				if (in_array($row['id_member'], $buddiesArray))
1395
					continue;
1396
				else
1397
					$buddiesArray[] = (int) $row['id_member'];
1398
			}
1399
			$smcFunc['db_free_result']($request);
1400
1401
			// Now update the current users buddy list.
1402
			$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1403
			updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1404
		}
1405
1406
		// Back to the buddy list!
1407
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1408
	}
1409
1410
	// Get all the users "buddies"...
1411
	$buddies = array();
1412
1413
	// Gotta load the custom profile fields names.
1414
	$request = $smcFunc['db_query']('', '
1415
		SELECT col_name, field_name, field_desc, field_type, bbc, enclose
1416
		FROM {db_prefix}custom_fields
1417
		WHERE active = {int:active}
1418
			AND private < {int:private_level}',
1419
		array(
1420
			'active' => 1,
1421
			'private_level' => 2,
1422
		)
1423
	);
1424
1425
	$context['custom_pf'] = array();
1426
	$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...
1427
	while ($row = $smcFunc['db_fetch_assoc']($request))
1428
		if (!isset($disabled_fields[$row['col_name']]))
1429
			$context['custom_pf'][$row['col_name']] = array(
1430
				'label' => $row['field_name'],
1431
				'type' => $row['field_type'],
1432
				'bbc' => !empty($row['bbc']),
1433
				'enclose' => $row['enclose'],
1434
			);
1435
1436
	// Gotta disable the gender option.
1437
	if (isset($context['custom_pf']['cust_gender']) && $context['custom_pf']['cust_gender'] == 'Disabled')
1438
		unset($context['custom_pf']['cust_gender']);
1439
1440
	$smcFunc['db_free_result']($request);
1441
1442 View Code Duplication
	if (!empty($buddiesArray))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1443
	{
1444
		$result = $smcFunc['db_query']('', '
1445
			SELECT id_member
1446
			FROM {db_prefix}members
1447
			WHERE id_member IN ({array_int:buddy_list})
1448
			ORDER BY real_name
1449
			LIMIT {int:buddy_list_count}',
1450
			array(
1451
				'buddy_list' => $buddiesArray,
1452
				'buddy_list_count' => substr_count($user_profile[$memID]['buddy_list'], ',') + 1,
1453
			)
1454
		);
1455
		while ($row = $smcFunc['db_fetch_assoc']($result))
1456
			$buddies[] = $row['id_member'];
1457
		$smcFunc['db_free_result']($result);
1458
	}
1459
1460
	$context['buddy_count'] = count($buddies);
1461
1462
	// Load all the members up.
1463
	loadMemberData($buddies, false, 'profile');
1464
1465
	// Setup the context for each buddy.
1466
	$context['buddies'] = array();
1467
	foreach ($buddies as $buddy)
1468
	{
1469
		loadMemberContext($buddy);
1470
		$context['buddies'][$buddy] = $memberContext[$buddy];
1471
1472
		// Make sure to load the appropriate fields for each user
1473
		if (!empty($context['custom_pf']))
1474
		{
1475
			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...
1476
			{
1477
				// Don't show anything if there isn't anything to show.
1478 View Code Duplication
				if (!isset($context['buddies'][$buddy]['options'][$key]))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1479
				{
1480
					$context['buddies'][$buddy]['options'][$key] = '';
1481
					continue;
1482
				}
1483
1484 View Code Duplication
				if ($column['bbc'] && !empty($context['buddies'][$buddy]['options'][$key]))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1485
					$context['buddies'][$buddy]['options'][$key] = strip_tags(parse_bbc($context['buddies'][$buddy]['options'][$key]));
1486
1487
				elseif ($column['type'] == 'check')
1488
					$context['buddies'][$buddy]['options'][$key] = $context['buddies'][$buddy]['options'][$key] == 0 ? $txt['no'] : $txt['yes'];
1489
1490
				// Enclosing the user input within some other text?
1491
				if (!empty($column['enclose']) && !empty($context['buddies'][$buddy]['options'][$key]))
1492
					$context['buddies'][$buddy]['options'][$key] = strtr($column['enclose'], array(
1493
						'{SCRIPTURL}' => $scripturl,
1494
						'{IMAGES_URL}' => $settings['images_url'],
1495
						'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1496
						'{INPUT}' => $context['buddies'][$buddy]['options'][$key],
1497
					));
1498
			}
1499
		}
1500
	}
1501
1502 View Code Duplication
	if (isset($_SESSION['prf-save']))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1503
	{
1504
		if ($_SESSION['prf-save'] === true)
1505
			$context['saved_successful'] = true;
1506
		else
1507
			$context['saved_failed'] = $_SESSION['prf-save'];
1508
1509
		unset($_SESSION['prf-save']);
1510
	}
1511
1512
	call_integration_hook('integrate_view_buddies', array($memID));
1513
}
1514
1515
/**
1516
 * Allows the user to view their ignore list, as well as the option to manage members on it.
1517
 *
1518
 * @param int $memID The ID of the member
1519
 */
1520
function editIgnoreList($memID)
1521
{
1522
	global $txt;
1523
	global $context, $user_profile, $memberContext, $smcFunc;
1524
1525
	// For making changes!
1526
	$ignoreArray = explode(',', $user_profile[$memID]['pm_ignore_list']);
1527
	foreach ($ignoreArray as $k => $dummy)
1528
		if ($dummy == '')
1529
			unset($ignoreArray[$k]);
1530
1531
	// Removing a member from the ignore list?
1532
	if (isset($_GET['remove']))
1533
	{
1534
		checkSession('get');
1535
1536
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1537
1538
		// Heh, I'm lazy, do it the easy way...
1539
		foreach ($ignoreArray as $key => $id_remove)
1540
			if ($id_remove == (int) $_GET['remove'])
1541
			{
1542
				unset($ignoreArray[$key]);
1543
				$_SESSION['prf-save'] = true;
1544
			}
1545
1546
		// Make the changes.
1547
		$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1548
		updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1549
1550
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1551
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1552
	}
1553
	elseif (isset($_POST['new_ignore']))
1554
	{
1555
		checkSession();
1556
		// Prepare the string for extraction...
1557
		$_POST['new_ignore'] = strtr($smcFunc['htmlspecialchars']($_POST['new_ignore'], ENT_QUOTES), array('&quot;' => '"'));
1558
		preg_match_all('~"([^"]+)"~', $_POST['new_ignore'], $matches);
1559
		$new_entries = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_ignore']))));
1560
1561 View Code Duplication
		foreach ($new_entries as $k => $dummy)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1562
		{
1563
			$new_entries[$k] = strtr(trim($new_entries[$k]), array('\'' => '&#039;'));
1564
1565
			if (strlen($new_entries[$k]) == 0 || in_array($new_entries[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1566
				unset($new_entries[$k]);
1567
		}
1568
1569
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1570 View Code Duplication
		if (!empty($new_entries))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1571
		{
1572
			// Now find out the id_member for the members in question.
1573
			$request = $smcFunc['db_query']('', '
1574
				SELECT id_member
1575
				FROM {db_prefix}members
1576
				WHERE member_name IN ({array_string:new_entries}) OR real_name IN ({array_string:new_entries})
1577
				LIMIT {int:count_new_entries}',
1578
				array(
1579
					'new_entries' => $new_entries,
1580
					'count_new_entries' => count($new_entries),
1581
				)
1582
			);
1583
1584
			if ($smcFunc['db_num_rows']($request) != 0)
1585
				$_SESSION['prf-save'] = true;
1586
1587
			// Add the new member to the buddies array.
1588
			while ($row = $smcFunc['db_fetch_assoc']($request))
1589
			{
1590
				if (in_array($row['id_member'], $ignoreArray))
1591
					continue;
1592
				else
1593
					$ignoreArray[] = (int) $row['id_member'];
1594
			}
1595
			$smcFunc['db_free_result']($request);
1596
1597
			// Now update the current users buddy list.
1598
			$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1599
			updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1600
		}
1601
1602
		// Back to the list of pityful people!
1603
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1604
	}
1605
1606
	// Initialise the list of members we're ignoring.
1607
	$ignored = array();
1608
1609 View Code Duplication
	if (!empty($ignoreArray))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1610
	{
1611
		$result = $smcFunc['db_query']('', '
1612
			SELECT id_member
1613
			FROM {db_prefix}members
1614
			WHERE id_member IN ({array_int:ignore_list})
1615
			ORDER BY real_name
1616
			LIMIT {int:ignore_list_count}',
1617
			array(
1618
				'ignore_list' => $ignoreArray,
1619
				'ignore_list_count' => substr_count($user_profile[$memID]['pm_ignore_list'], ',') + 1,
1620
			)
1621
		);
1622
		while ($row = $smcFunc['db_fetch_assoc']($result))
1623
			$ignored[] = $row['id_member'];
1624
		$smcFunc['db_free_result']($result);
1625
	}
1626
1627
	$context['ignore_count'] = count($ignored);
1628
1629
	// Load all the members up.
1630
	loadMemberData($ignored, false, 'profile');
1631
1632
	// Setup the context for each buddy.
1633
	$context['ignore_list'] = array();
1634
	foreach ($ignored as $ignore_member)
1635
	{
1636
		loadMemberContext($ignore_member);
1637
		$context['ignore_list'][$ignore_member] = $memberContext[$ignore_member];
1638
	}
1639
1640 View Code Duplication
	if (isset($_SESSION['prf-save']))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1641
	{
1642
		if ($_SESSION['prf-save'] === true)
1643
			$context['saved_successful'] = true;
1644
		else
1645
			$context['saved_failed'] = $_SESSION['prf-save'];
1646
1647
		unset($_SESSION['prf-save']);
1648
	}
1649
}
1650
1651
/**
1652
 * Handles the account section of the profile
1653
 *
1654
 * @param int $memID The ID of the member
1655
 */
1656
function account($memID)
1657
{
1658
	global $context, $txt;
1659
1660
	loadThemeOptions($memID);
1661
	if (allowedTo(array('profile_identity_own', 'profile_identity_any', 'profile_password_own', 'profile_password_any')))
1662
		loadCustomFields($memID, 'account');
1663
1664
	$context['sub_template'] = 'edit_options';
1665
	$context['page_desc'] = $txt['account_info'];
1666
1667
	setupProfileContext(
1668
		array(
1669
			'member_name', 'real_name', 'date_registered', 'posts', 'lngfile', 'hr',
1670
			'id_group', 'hr',
1671
			'email_address', 'show_online', 'hr',
1672
			'tfa', 'hr',
1673
			'passwrd1', 'passwrd2', 'hr',
1674
			'secret_question', 'secret_answer',
1675
		)
1676
	);
1677
}
1678
1679
/**
1680
 * Handles the main "Forum Profile" section of the profile
1681
 *
1682
 * @param int $memID The ID of the member
1683
 */
1684
function forumProfile($memID)
1685
{
1686
	global $context, $txt;
1687
1688
	loadThemeOptions($memID);
1689
	if (allowedTo(array('profile_forum_own', 'profile_forum_any')))
1690
		loadCustomFields($memID, 'forumprofile');
1691
1692
	$context['sub_template'] = 'edit_options';
1693
	$context['page_desc'] = $txt['forumProfile_info'];
1694
	$context['show_preview_button'] = true;
1695
1696
	setupProfileContext(
1697
		array(
1698
			'avatar_choice', 'hr', 'personal_text', 'hr',
1699
			'bday1', 'usertitle', 'signature', 'hr',
1700
			'website_title', 'website_url',
1701
		)
1702
	);
1703
}
1704
1705
/**
1706
 * Recursive function to retrieve server-stored avatar files
1707
 *
1708
 * @param string $directory The directory to look for files in
1709
 * @param int $level How many levels we should go in the directory
1710
 * @return array An array of information about the files and directories found
1711
 */
1712
function getAvatars($directory, $level)
1713
{
1714
	global $context, $txt, $modSettings, $smcFunc;
1715
1716
	$result = array();
1717
1718
	// Open the directory..
1719
	$dir = dir($modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory);
1720
	$dirs = array();
1721
	$files = array();
1722
1723
	if (!$dir)
1724
		return array();
1725
1726
	while ($line = $dir->read())
1727
	{
1728
		if (in_array($line, array('.', '..', 'blank.png', 'index.php')))
1729
			continue;
1730
1731
		if (is_dir($modSettings['avatar_directory'] . '/' . $directory . (!empty($directory) ? '/' : '') . $line))
1732
			$dirs[] = $line;
1733
		else
1734
			$files[] = $line;
1735
	}
1736
	$dir->close();
1737
1738
	// Sort the results...
1739
	natcasesort($dirs);
1740
	natcasesort($files);
1741
1742
	if ($level == 0)
1743
	{
1744
		$result[] = array(
1745
			'filename' => 'blank.png',
1746
			'checked' => in_array($context['member']['avatar']['server_pic'], array('', 'blank.png')),
1747
			'name' => $txt['no_pic'],
1748
			'is_dir' => false
1749
		);
1750
	}
1751
1752
	foreach ($dirs as $line)
1753
	{
1754
		$tmp = getAvatars($directory . (!empty($directory) ? '/' : '') . $line, $level + 1);
1755
		if (!empty($tmp))
1756
			$result[] = array(
1757
				'filename' => $smcFunc['htmlspecialchars']($line),
1758
				'checked' => strpos($context['member']['avatar']['server_pic'], $line . '/') !== false,
1759
				'name' => '[' . $smcFunc['htmlspecialchars'](str_replace('_', ' ', $line)) . ']',
1760
				'is_dir' => true,
1761
				'files' => $tmp
1762
		);
1763
		unset($tmp);
1764
	}
1765
1766
	foreach ($files as $line)
1767
	{
1768
		$filename = substr($line, 0, (strlen($line) - strlen(strrchr($line, '.'))));
1769
		$extension = substr(strrchr($line, '.'), 1);
1770
1771
		// Make sure it is an image.
1772
		if (strcasecmp($extension, 'gif') != 0 && strcasecmp($extension, 'jpg') != 0 && strcasecmp($extension, 'jpeg') != 0 && strcasecmp($extension, 'png') != 0 && strcasecmp($extension, 'bmp') != 0)
1773
			continue;
1774
1775
		$result[] = array(
1776
			'filename' => $smcFunc['htmlspecialchars']($line),
1777
			'checked' => $line == $context['member']['avatar']['server_pic'],
1778
			'name' => $smcFunc['htmlspecialchars'](str_replace('_', ' ', $filename)),
1779
			'is_dir' => false
1780
		);
1781
		if ($level == 1)
1782
			$context['avatar_list'][] = $directory . '/' . $line;
1783
	}
1784
1785
	return $result;
1786
}
1787
1788
/**
1789
 * Handles the "Look and Layout" section of the profile
1790
 *
1791
 * @param int $memID The ID of the member
1792
 */
1793
function theme($memID)
1794
{
1795
	global $txt, $context;
1796
1797
	loadThemeOptions($memID);
1798
	if (allowedTo(array('profile_extra_own', 'profile_extra_any')))
1799
		loadCustomFields($memID, 'theme');
1800
1801
	$context['sub_template'] = 'edit_options';
1802
	$context['page_desc'] = $txt['theme_info'];
1803
1804
	setupProfileContext(
1805
		array(
1806
			'id_theme', 'smiley_set', 'hr',
1807
			'time_format', 'timezone', 'hr',
1808
			'theme_settings',
1809
		)
1810
	);
1811
}
1812
1813
/**
1814
 * Display the notifications and settings for changes.
1815
 *
1816
 * @param int $memID The ID of the member
1817
 */
1818
function notification($memID)
1819
{
1820
	global $txt, $context;
1821
1822
	// Going to want this for consistency.
1823
	loadCSSFile('admin.css', array(), 'smf_admin');
1824
1825
	// This is just a bootstrap for everything else.
1826
	$sa = array(
1827
		'alerts' => 'alert_configuration',
1828
		'markread' => 'alert_markread',
1829
		'topics' => 'alert_notifications_topics',
1830
		'boards' => 'alert_notifications_boards',
1831
	);
1832
1833
	$subAction = !empty($_GET['sa']) && isset($sa[$_GET['sa']]) ? $_GET['sa'] : 'alerts';
1834
1835
	$context['sub_template'] = $sa[$subAction];
1836
	$context[$context['profile_menu_name']]['tab_data'] = array(
1837
		'title' => $txt['notification'],
1838
		'help' => '',
1839
		'description' => $txt['notification_info'],
1840
	);
1841
	$sa[$subAction]($memID);
1842
}
1843
1844
/**
1845
 * Handles configuration of alert preferences
1846
 *
1847
 * @param int $memID The ID of the member
1848
 */
1849
function alert_configuration($memID)
1850
{
1851
	global $txt, $user_profile, $context, $modSettings, $smcFunc, $sourcedir;
1852
1853
	if (!isset($context['token_check']))
1854
		$context['token_check'] = 'profile-nt' . $memID;
1855
1856
	is_not_guest();
1857
	if (!$context['user']['is_owner'])
1858
		isAllowedTo('profile_extra_any');
1859
1860
	// Set the post action if we're coming from the profile...
1861
	if (!isset($context['action']))
1862
		$context['action'] = 'action=profile;area=notification;sa=alerts;u=' . $memID;
1863
1864
	// What options are set
1865
	loadThemeOptions($memID);
1866
	loadJavaScriptFile('alertSettings.js', array(), 'smf_alertSettings');
1867
1868
	// Now load all the values for this user.
1869
	require_once($sourcedir . '/Subs-Notify.php');
1870
	$prefs = getNotifyPrefs($memID, '', $memID != 0);
1871
1872
	$context['alert_prefs'] = !empty($prefs[$memID]) ? $prefs[$memID] : array();
1873
1874
	$context['member'] += array(
1875
		'alert_timeout' => isset($context['alert_prefs']['alert_timeout']) ? $context['alert_prefs']['alert_timeout'] : 10,
1876
		'notify_announcements' => isset($context['alert_prefs']['announcements']) ? $context['alert_prefs']['announcements'] : 0,
1877
	);
1878
1879
	// Now for the exciting stuff.
1880
	// We have groups of items, each item has both an alert and an email key as well as an optional help string.
1881
	// Valid values for these keys are 'always', 'yes', 'never'; if using always or never you should add a help string.
1882
	$alert_types = array(
1883
		'board' => array(
1884
			'topic_notify' => array('alert' => 'yes', 'email' => 'yes'),
1885
			'board_notify' => array('alert' => 'yes', 'email' => 'yes'),
1886
		),
1887
		'msg' => array(
1888
			'msg_mention' => array('alert' => 'yes', 'email' => 'yes'),
1889
			'msg_quote' => array('alert' => 'yes', 'email' => 'yes'),
1890
			'msg_like' => array('alert' => 'yes', 'email' => 'never'),
1891
			'unapproved_reply' => array('alert' => 'yes', 'email' => 'yes'),
1892
		),
1893
		'pm' => array(
1894
			'pm_new' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_read', 'is_board' => false)),
1895
			'pm_reply' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_send', 'is_board' => false)),
1896
		),
1897
		'groupr' => array(
1898
			'groupr_approved' => array('alert' => 'always', 'email' => 'yes'),
1899
			'groupr_rejected' => array('alert' => 'always', 'email' => 'yes'),
1900
		),
1901
		'moderation' => array(
1902
			'unapproved_post' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'approve_posts', 'is_board' => true)),
1903
			'msg_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1904
			'msg_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1905
			'member_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1906
			'member_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1907
		),
1908
		'members' => array(
1909
			'member_register' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1910
			'request_group' => array('alert' => 'yes', 'email' => 'yes'),
1911
			'warn_any' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'issue_warning', 'is_board' => false)),
1912
			'buddy_request'  => array('alert' => 'yes', 'email' => 'never'),
1913
			'birthday'  => array('alert' => 'yes', 'email' => 'yes'),
1914
		),
1915
		'calendar' => array(
1916
			'event_new' => array('alert' => 'yes', 'email' => 'yes', 'help' => 'alert_event_new'),
1917
		),
1918
		'paidsubs' => array(
1919
			'paidsubs_expiring' => array('alert' => 'yes', 'email' => 'yes'),
1920
		),
1921
	);
1922
	$group_options = array(
1923
		'board' => array(
1924
			array('check', 'msg_auto_notify', 'label' => 'after'),
1925
			array('check', 'msg_receive_body', 'label' => 'after'),
1926
			array('select', 'msg_notify_pref', 'label' => 'before', 'opts' => array(
1927
				0 => $txt['alert_opt_msg_notify_pref_nothing'],
1928
				1 => $txt['alert_opt_msg_notify_pref_instant'],
1929
				2 => $txt['alert_opt_msg_notify_pref_first'],
1930
				3 => $txt['alert_opt_msg_notify_pref_daily'],
1931
				4 => $txt['alert_opt_msg_notify_pref_weekly'],
1932
			)),
1933
			array('select', 'msg_notify_type', 'label' => 'before', 'opts' => array(
1934
				1 => $txt['notify_send_type_everything'],
1935
				2 => $txt['notify_send_type_everything_own'],
1936
				3 => $txt['notify_send_type_only_replies'],
1937
				4 => $txt['notify_send_type_nothing'],
1938
			)),
1939
		),
1940
		'pm' => array(
1941
			array('select', 'pm_notify', 'label' => 'before', 'opts' => array(
1942
				1 => $txt['email_notify_all'],
1943
				2 => $txt['email_notify_buddies'],
1944
			)),
1945
		),
1946
	);
1947
1948
	// There are certain things that are disabled at the group level.
1949
	if (empty($modSettings['cal_enabled']))
1950
		unset($alert_types['calendar']);
1951
1952
	// Disable paid subscriptions at group level if they're disabled
1953
	if (empty($modSettings['paid_enabled']))
1954
		unset($alert_types['paidsubs']);
1955
1956
	// Disable membergroup requests at group level if they're disabled
1957
	if (empty($modSettings['show_group_membership']))
1958
		unset($alert_types['groupr'], $alert_types['members']['request_group']);
1959
1960
	// Disable mentions if they're disabled
1961
	if (empty($modSettings['enable_mentions']))
1962
		unset($alert_types['msg']['msg_mention']);
1963
1964
	// Disable likes if they're disabled
1965
	if (empty($modSettings['enable_likes']))
1966
		unset($alert_types['msg']['msg_like']);
1967
1968
	// Disable buddy requests if they're disabled
1969
	if (empty($modSettings['enable_buddylist']))
1970
		unset($alert_types['members']['buddy_request']);
1971
1972
	// Now, now, we could pass this through global but we should really get into the habit of
1973
	// passing content to hooks, not expecting hooks to splatter everything everywhere.
1974
	call_integration_hook('integrate_alert_types', array(&$alert_types, &$group_options));
1975
1976
	// Now we have to do some permissions testing - but only if we're not loading this from the admin center
1977
	if (!empty($memID))
1978
	{
1979
		require_once($sourcedir . '/Subs-Members.php');
1980
		$perms_cache = array();
1981
		$request = $smcFunc['db_query']('', '
1982
			SELECT COUNT(*)
1983
			FROM {db_prefix}group_moderators
1984
			WHERE id_member = {int:memID}',
1985
			array(
1986
				'memID' => $memID,
1987
			)
1988
		);
1989
1990
		list ($can_mod) = $smcFunc['db_fetch_row']($request);
1991
1992
		if (!isset($perms_cache['manage_membergroups']))
1993
		{
1994
			$members = membersAllowedTo('manage_membergroups');
1995
			$perms_cache['manage_membergroups'] = in_array($memID, $members);
1996
		}
1997
1998
		if (!($perms_cache['manage_membergroups'] || $can_mod != 0))
1999
			unset($alert_types['members']['request_group']);
2000
2001
		foreach ($alert_types as $group => $items)
2002
		{
2003
			foreach ($items as $alert_key => $alert_value)
2004
			{
2005
				if (!isset($alert_value['permission']))
2006
					continue;
2007
				if (!isset($perms_cache[$alert_value['permission']['name']]))
2008
				{
2009
					$in_board = !empty($alert_value['permission']['is_board']) ? 0 : null;
2010
					$members = membersAllowedTo($alert_value['permission']['name'], $in_board);
2011
					$perms_cache[$alert_value['permission']['name']] = in_array($memID, $members);
2012
				}
2013
2014
				if (!$perms_cache[$alert_value['permission']['name']])
2015
					unset ($alert_types[$group][$alert_key]);
2016
			}
2017
2018
			if (empty($alert_types[$group]))
2019
				unset ($alert_types[$group]);
2020
		}
2021
	}
2022
2023
	// And finally, exporting it to be useful later.
2024
	$context['alert_types'] = $alert_types;
2025
	$context['alert_group_options'] = $group_options;
2026
2027
	$context['alert_bits'] = array(
2028
		'alert' => 0x01,
2029
		'email' => 0x02,
2030
	);
2031
2032
	if (isset($_POST['notify_submit']))
2033
	{
2034
		checkSession();
2035
		validateToken($context['token_check'], 'post');
2036
2037
		// We need to step through the list of valid settings and figure out what the user has set.
2038
		$update_prefs = array();
2039
2040
		// Now the group level options
2041
		foreach ($context['alert_group_options'] as $opt_group => $group)
2042
		{
2043
			foreach ($group as $this_option)
2044
			{
2045
				switch ($this_option[0])
2046
				{
2047
					case 'check':
2048
						$update_prefs[$this_option[1]] = !empty($_POST['opt_' . $this_option[1]]) ? 1 : 0;
2049
						break;
2050
					case 'select':
2051
						if (isset($_POST['opt_' . $this_option[1]], $this_option['opts'][$_POST['opt_' . $this_option[1]]]))
2052
							$update_prefs[$this_option[1]] = $_POST['opt_' . $this_option[1]];
2053
						else
2054
						{
2055
							// We didn't have a sane value. Let's grab the first item from the possibles.
2056
							$keys = array_keys($this_option['opts']);
2057
							$first = array_shift($keys);
2058
							$update_prefs[$this_option[1]] = $first;
2059
						}
2060
						break;
2061
				}
2062
			}
2063
		}
2064
2065
		// Now the individual options
2066
		foreach ($context['alert_types'] as $alert_group => $items)
2067
		{
2068
			foreach ($items as $item_key => $this_options)
2069
			{
2070
				$this_value = 0;
2071
				foreach ($context['alert_bits'] as $type => $bitvalue)
2072
				{
2073
					if ($this_options[$type] == 'yes' && !empty($_POST[$type . '_' . $item_key]) || $this_options[$type] == 'always')
2074
						$this_value |= $bitvalue;
2075
				}
2076
				if (!isset($context['alert_prefs'][$item_key]) || $context['alert_prefs'][$item_key] != $this_value)
2077
					$update_prefs[$item_key] = $this_value;
2078
			}
2079
		}
2080
2081
		if (!empty($_POST['opt_alert_timeout']))
2082
			$update_prefs['alert_timeout'] = $context['member']['alert_timeout'] = (int) $_POST['opt_alert_timeout'];
2083
2084
		if (!empty($_POST['notify_announcements']))
2085
			$update_prefs['announcements'] = $context['member']['notify_announcements'] = (int) $_POST['notify_announcements'];
2086
2087
		setNotifyPrefs((int) $memID, $update_prefs);
2088
		foreach ($update_prefs as $pref => $value)
2089
			$context['alert_prefs'][$pref] = $value;
2090
2091
		makeNotificationChanges($memID);
2092
2093
		$context['profile_updated'] = $txt['profile_updated_own'];
2094
	}
2095
2096
	createToken($context['token_check'], 'post');
2097
}
2098
2099
/**
2100
 * Marks all alerts as read for the specified user
2101
 *
2102
 * @param int $memID The ID of the member
2103
 */
2104
function alert_markread($memID)
2105
{
2106
	global $context, $db_show_debug, $smcFunc;
2107
2108
	// We do not want to output debug information here.
2109
	$db_show_debug = false;
2110
2111
	// We only want to output our little layer here.
2112
	$context['template_layers'] = array();
2113
	$context['sub_template'] = 'alerts_all_read';
2114
2115
	loadLanguage('Alerts');
2116
2117
	// Now we're all set up.
2118
	is_not_guest();
2119
	if (!$context['user']['is_owner'])
2120
		fatal_error('no_access');
2121
2122
	checkSession('get');
2123
2124
	// Assuming we're here, mark everything as read and head back.
2125
	// We only spit back the little layer because this should be called AJAXively.
2126
	$smcFunc['db_query']('', '
2127
		UPDATE {db_prefix}user_alerts
2128
		SET is_read = {int:now}
2129
		WHERE id_member = {int:current_member}
2130
			AND is_read = 0',
2131
		array(
2132
			'now' => time(),
2133
			'current_member' => $memID,
2134
		)
2135
	);
2136
2137
	updateMemberData($memID, array('alerts' => 0));
2138
}
2139
2140
/**
2141
 * Marks a group of alerts as un/read
2142
 *
2143
 * @param int $memID The user ID.
2144
 * @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.
2145
 * @param integer $read To mark as read or unread, 1 for read, 0 or any other value different than 1 for unread.
2146
 * @return integer How many alerts remain unread
2147
 */
2148
function alert_mark($memID, $toMark, $read = 0)
2149
{
2150
	global $smcFunc;
2151
2152
	if (empty($toMark) || empty($memID))
2153
		return false;
2154
2155
	$toMark = (array) $toMark;
2156
2157
	$smcFunc['db_query']('', '
2158
		UPDATE {db_prefix}user_alerts
2159
		SET is_read = {int:read}
2160
		WHERE id_alert IN({array_int:toMark})',
2161
		array(
2162
			'read' => $read == 1 ? time() : 0,
2163
			'toMark' => $toMark,
2164
		)
2165
	);
2166
2167
	// Gotta know how many unread alerts are left.
2168
	$count = alert_count($memID, true);
2169
2170
	updateMemberData($memID, array('alerts' => $count));
2171
2172
	// Might want to know this.
2173
	return $count;
2174
}
2175
2176
/**
2177
 * Deletes a single or a group of alerts by ID
2178
 *
2179
 * @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.
2180
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2181
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
2182
 */
2183
function alert_delete($toDelete, $memID = false)
2184
{
2185
	global $smcFunc;
2186
2187
	if (empty($toDelete))
2188
		return false;
2189
2190
	$toDelete = (array) $toDelete;
2191
2192
	$smcFunc['db_query']('', '
2193
		DELETE FROM {db_prefix}user_alerts
2194
		WHERE id_alert IN({array_int:toDelete})',
2195
		array(
2196
			'toDelete' => $toDelete,
2197
		)
2198
	);
2199
2200
	// Gotta know how many unread alerts are left.
2201
	if ($memID)
2202
	{
2203
		$count = alert_count($memID, true);
0 ignored issues
show
Bug introduced by
It seems like $memID defined by parameter $memID on line 2183 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...
2204
2205
		updateMemberData($memID, array('alerts' => $count));
2206
2207
		// Might want to know this.
2208
		return $count;
2209
	}
2210
}
2211
2212
/**
2213
 * Counts how many alerts a user has - either unread or all depending on $unread
2214
 *
2215
 * @param int $memID The user ID.
2216
 * @param bool $unread Whether to only count unread alerts.
2217
 * @return int The number of requested alerts
2218
 */
2219
function alert_count($memID, $unread = false)
2220
{
2221
	global $smcFunc;
2222
2223
	if (empty($memID))
2224
		return false;
2225
2226
	$request = $smcFunc['db_query']('', '
2227
		SELECT id_alert
2228
		FROM {db_prefix}user_alerts
2229
		WHERE id_member = {int:id_member}
2230
			'.($unread ? '
2231
			AND is_read = 0' : ''),
2232
		array(
2233
			'id_member' => $memID,
2234
		)
2235
	);
2236
2237
	$count = $smcFunc['db_num_rows']($request);
2238
	$smcFunc['db_free_result']($request);
2239
2240
	return $count;
2241
}
2242
2243
/**
2244
 * Handles alerts related to topics and posts
2245
 *
2246
 * @param int $memID The ID of the member
2247
 */
2248
function alert_notifications_topics($memID)
2249
{
2250
	global $txt, $scripturl, $context, $modSettings, $sourcedir;
2251
2252
	// Because of the way this stuff works, we want to do this ourselves.
2253 View Code Duplication
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2254
	{
2255
		checkSession();
2256
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2257
2258
		makeNotificationChanges($memID);
2259
		$context['profile_updated'] = $txt['profile_updated_own'];
2260
	}
2261
2262
	// Now set up for the token check.
2263
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2264
	createToken($context['token_check'], 'post');
2265
2266
	// Gonna want this for the list.
2267
	require_once($sourcedir . '/Subs-List.php');
2268
2269
	// Do the topic notifications.
2270
	$listOptions = array(
2271
		'id' => 'topic_notification_list',
2272
		'width' => '100%',
2273
		'items_per_page' => $modSettings['defaultMaxListItems'],
2274
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2275
		'no_items_align' => 'left',
2276
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2277
		'default_sort_col' => 'last_post',
2278
		'get_items' => array(
2279
			'function' => 'list_getTopicNotifications',
2280
			'params' => array(
2281
				$memID,
2282
			),
2283
		),
2284
		'get_count' => array(
2285
			'function' => 'list_getTopicNotificationCount',
2286
			'params' => array(
2287
				$memID,
2288
			),
2289
		),
2290
		'columns' => array(
2291
			'subject' => array(
2292
				'header' => array(
2293
					'value' => $txt['notifications_topics'],
2294
					'class' => 'lefttext',
2295
				),
2296
				'data' => array(
2297
					'function' => function($topic) use ($txt)
2298
					{
2299
						$link = $topic['link'];
2300
2301
						if ($topic['new'])
2302
							$link .= ' <a href="' . $topic['new_href'] . '"><span class="new_posts">' . $txt['new'] . '</span></a>';
2303
2304
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2305
2306
						return $link;
2307
					},
2308
				),
2309
				'sort' => array(
2310
					'default' => 'ms.subject',
2311
					'reverse' => 'ms.subject DESC',
2312
				),
2313
			),
2314
			'started_by' => array(
2315
				'header' => array(
2316
					'value' => $txt['started_by'],
2317
					'class' => 'lefttext',
2318
				),
2319
				'data' => array(
2320
					'db' => 'poster_link',
2321
				),
2322
				'sort' => array(
2323
					'default' => 'real_name_col',
2324
					'reverse' => 'real_name_col DESC',
2325
				),
2326
			),
2327
			'last_post' => array(
2328
				'header' => array(
2329
					'value' => $txt['last_post'],
2330
					'class' => 'lefttext',
2331
				),
2332
				'data' => array(
2333
					'sprintf' => array(
2334
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2335
						'params' => array(
2336
							'updated' => false,
2337
							'poster_updated_link' => false,
2338
						),
2339
					),
2340
				),
2341
				'sort' => array(
2342
					'default' => 'ml.id_msg DESC',
2343
					'reverse' => 'ml.id_msg',
2344
				),
2345
			),
2346
			'alert' => array(
2347
				'header' => array(
2348
					'value' => $txt['notify_what_how'],
2349
					'class' => 'lefttext',
2350
				),
2351
				'data' => array(
2352
					'function' => function($topic) use ($txt)
2353
					{
2354
						$pref = $topic['notify_pref'];
2355
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2356
						return $txt['notify_topic_' . $mode];
2357
					},
2358
				),
2359
			),
2360
			'delete' => array(
2361
				'header' => array(
2362
					'value' => '<input type="checkbox" class="input_check" onclick="invertAll(this, this.form);">',
2363
					'style' => 'width: 4%;',
2364
					'class' => 'centercol',
2365
				),
2366
				'data' => array(
2367
					'sprintf' => array(
2368
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d" class="input_check">',
2369
						'params' => array(
2370
							'id' => false,
2371
						),
2372
					),
2373
					'class' => 'centercol',
2374
				),
2375
			),
2376
		),
2377
		'form' => array(
2378
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2379
			'include_sort' => true,
2380
			'include_start' => true,
2381
			'hidden_fields' => array(
2382
				'u' => $memID,
2383
				'sa' => $context['menu_item_selected'],
2384
				$context['session_var'] => $context['session_id'],
2385
			),
2386
			'token' => $context['token_check'],
2387
		),
2388
		'additional_rows' => array(
2389
			array(
2390
				'position' => 'bottom_of_list',
2391
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button_submit" />
2392
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button_submit" />',
2393
				'class' => 'floatright',
2394
			),
2395
		),
2396
	);
2397
2398
	// Create the notification list.
2399
	createList($listOptions);
2400
}
2401
2402
/**
2403
 * Handles preferences related to board-level notifications
2404
 *
2405
 * @param int $memID The ID of the member
2406
 */
2407
function alert_notifications_boards($memID)
2408
{
2409
	global $txt, $scripturl, $context, $sourcedir;
2410
2411
	// Because of the way this stuff works, we want to do this ourselves.
2412 View Code Duplication
	if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
1 ignored issue
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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2413
	{
2414
		checkSession();
2415
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2416
2417
		makeNotificationChanges($memID);
2418
		$context['profile_updated'] = $txt['profile_updated_own'];
2419
	}
2420
2421
	// Now set up for the token check.
2422
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2423
	createToken($context['token_check'], 'post');
2424
2425
	// Gonna want this for the list.
2426
	require_once($sourcedir . '/Subs-List.php');
2427
2428
	// Fine, start with the board list.
2429
	$listOptions = array(
2430
		'id' => 'board_notification_list',
2431
		'width' => '100%',
2432
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2433
		'no_items_align' => 'left',
2434
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2435
		'default_sort_col' => 'board_name',
2436
		'get_items' => array(
2437
			'function' => 'list_getBoardNotifications',
2438
			'params' => array(
2439
				$memID,
2440
			),
2441
		),
2442
		'columns' => array(
2443
			'board_name' => array(
2444
				'header' => array(
2445
					'value' => $txt['notifications_boards'],
2446
					'class' => 'lefttext',
2447
				),
2448
				'data' => array(
2449
					'function' => function($board) use ($txt)
2450
					{
2451
						$link = $board['link'];
2452
2453
						if ($board['new'])
2454
							$link .= ' <a href="' . $board['href'] . '"><span class="new_posts">' . $txt['new'] . '</span></a>';
2455
2456
						return $link;
2457
					},
2458
				),
2459
				'sort' => array(
2460
					'default' => 'name',
2461
					'reverse' => 'name DESC',
2462
				),
2463
			),
2464
			'alert' => array(
2465
				'header' => array(
2466
					'value' => $txt['notify_what_how'],
2467
					'class' => 'lefttext',
2468
				),
2469
				'data' => array(
2470
					'function' => function($board) use ($txt)
2471
					{
2472
						$pref = $board['notify_pref'];
2473
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2474
						return $txt['notify_board_' . $mode];
2475
					},
2476
				),
2477
			),
2478
			'delete' => array(
2479
				'header' => array(
2480
					'value' => '<input type="checkbox" class="input_check" onclick="invertAll(this, this.form);">',
2481
					'style' => 'width: 4%;',
2482
					'class' => 'centercol',
2483
				),
2484
				'data' => array(
2485
					'sprintf' => array(
2486
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d" class="input_check">',
2487
						'params' => array(
2488
							'id' => false,
2489
						),
2490
					),
2491
					'class' => 'centercol',
2492
				),
2493
			),
2494
		),
2495
		'form' => array(
2496
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2497
			'include_sort' => true,
2498
			'include_start' => true,
2499
			'hidden_fields' => array(
2500
				'u' => $memID,
2501
				'sa' => $context['menu_item_selected'],
2502
				$context['session_var'] => $context['session_id'],
2503
			),
2504
			'token' => $context['token_check'],
2505
		),
2506
		'additional_rows' => array(
2507
			array(
2508
				'position' => 'bottom_of_list',
2509
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button_submit">
2510
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button_submit" />',
2511
				'class' => 'floatright',
2512
			),
2513
		),
2514
	);
2515
2516
	// Create the board notification list.
2517
	createList($listOptions);
2518
}
2519
2520
/**
2521
 * Determins how many topics a user has requested notifications for
2522
 *
2523
 * @param int $memID The ID of the member
2524
 * @return int The number of topic notifications for this user
2525
 */
2526
function list_getTopicNotificationCount($memID)
2527
{
2528
	global $smcFunc, $user_info, $modSettings;
2529
2530
	$request = $smcFunc['db_query']('', '
2531
		SELECT COUNT(*)
2532
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2533
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2534
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2535
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2536
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2537
			AND t.approved = {int:is_approved}' : ''),
2538
		array(
2539
			'selected_member' => $memID,
2540
			'is_approved' => 1,
2541
		)
2542
	);
2543
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2544
	$smcFunc['db_free_result']($request);
2545
2546
	return (int) $totalNotifications;
2547
}
2548
2549
/**
2550
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2551
 *
2552
 * @param int $start Which item to start with (for pagination purposes)
2553
 * @param int $items_per_page How many items to display on each page
2554
 * @param string $sort A string indicating how to sort the results
2555
 * @param int $memID The ID of the member
2556
 * @return array An array of information about the topics a user has subscribed to
2557
 */
2558
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2559
{
2560
	global $smcFunc, $scripturl, $user_info, $modSettings, $sourcedir;
2561
2562
	require_once($sourcedir . '/Subs-Notify.php');
2563
	$prefs = getNotifyPrefs($memID);
2564
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2565
2566
	// All the topics with notification on...
2567
	$request = $smcFunc['db_query']('', '
2568
		SELECT
2569
			COALESCE(lt.id_msg, COALESCE(lmr.id_msg, -1)) + 1 AS new_from, b.id_board, b.name,
2570
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2571
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2572
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2573
			lt.unwatched
2574
		FROM {db_prefix}log_notify AS ln
2575
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2576
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2577
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2578
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2579
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2580
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2581
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2582
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2583
		WHERE ln.id_member = {int:selected_member}
2584
		ORDER BY {raw:sort}
2585
		LIMIT {int:offset}, {int:items_per_page}',
2586
		array(
2587
			'current_member' => $user_info['id'],
2588
			'is_approved' => 1,
2589
			'selected_member' => $memID,
2590
			'sort' => $sort,
2591
			'offset' => $start,
2592
			'items_per_page' => $items_per_page,
2593
		)
2594
	);
2595
	$notification_topics = array();
2596
	while ($row = $smcFunc['db_fetch_assoc']($request))
2597
	{
2598
		censorText($row['subject']);
2599
2600
		$notification_topics[] = array(
2601
			'id' => $row['id_topic'],
2602
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2603
			'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>',
2604
			'subject' => $row['subject'],
2605
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2606
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2607
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2608
			'new_from' => $row['new_from'],
2609
			'updated' => timeformat($row['poster_time']),
2610
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2611
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2612
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2613
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2614
			'unwatched' => $row['unwatched'],
2615
		);
2616
	}
2617
	$smcFunc['db_free_result']($request);
2618
2619
	return $notification_topics;
2620
}
2621
2622
/**
2623
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2624
 *
2625
 * @param int $start Which item to start with (not used here)
2626
 * @param int $items_per_page How many items to show on each page (not used here)
2627
 * @param string $sort A string indicating how to sort the results
2628
 * @param int $memID The ID of the member
2629
 * @return array An array of information about all the boards a user is subscribed to
2630
 */
2631
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2632
{
2633
	global $smcFunc, $scripturl, $user_info, $sourcedir;
2634
2635
	require_once($sourcedir . '/Subs-Notify.php');
2636
	$prefs = getNotifyPrefs($memID);
2637
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2638
2639
	$request = $smcFunc['db_query']('', '
2640
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2641
		FROM {db_prefix}log_notify AS ln
2642
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2643
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2644
		WHERE ln.id_member = {int:selected_member}
2645
			AND {query_see_board}
2646
		ORDER BY {raw:sort}',
2647
		array(
2648
			'current_member' => $user_info['id'],
2649
			'selected_member' => $memID,
2650
			'sort' => $sort,
2651
		)
2652
	);
2653
	$notification_boards = array();
2654
	while ($row = $smcFunc['db_fetch_assoc']($request))
2655
		$notification_boards[] = array(
2656
			'id' => $row['id_board'],
2657
			'name' => $row['name'],
2658
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2659
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2660
			'new' => $row['board_read'] < $row['id_msg_updated'],
2661
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2662
		);
2663
	$smcFunc['db_free_result']($request);
2664
2665
	return $notification_boards;
2666
}
2667
2668
/**
2669
 * Loads the theme options for a user
2670
 *
2671
 * @param int $memID The ID of the member
2672
 */
2673
function loadThemeOptions($memID)
2674
{
2675
	global $context, $options, $cur_profile, $smcFunc;
2676
2677 View Code Duplication
	if (isset($_POST['default_options']))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2678
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2679
2680
	if ($context['user']['is_owner'])
2681
	{
2682
		$context['member']['options'] = $options;
2683
		if (isset($_POST['options']) && is_array($_POST['options']))
2684
			foreach ($_POST['options'] as $k => $v)
2685
				$context['member']['options'][$k] = $v;
2686
	}
2687
	else
2688
	{
2689
		$request = $smcFunc['db_query']('', '
2690
			SELECT id_member, variable, value
2691
			FROM {db_prefix}themes
2692
			WHERE id_theme IN (1, {int:member_theme})
2693
				AND id_member IN (-1, {int:selected_member})',
2694
			array(
2695
				'member_theme' => (int) $cur_profile['id_theme'],
2696
				'selected_member' => $memID,
2697
			)
2698
		);
2699
		$temp = array();
2700
		while ($row = $smcFunc['db_fetch_assoc']($request))
2701
		{
2702
			if ($row['id_member'] == -1)
2703
			{
2704
				$temp[$row['variable']] = $row['value'];
2705
				continue;
2706
			}
2707
2708
			if (isset($_POST['options'][$row['variable']]))
2709
				$row['value'] = $_POST['options'][$row['variable']];
2710
			$context['member']['options'][$row['variable']] = $row['value'];
2711
		}
2712
		$smcFunc['db_free_result']($request);
2713
2714
		// Load up the default theme options for any missing.
2715
		foreach ($temp as $k => $v)
2716
		{
2717
			if (!isset($context['member']['options'][$k]))
2718
				$context['member']['options'][$k] = $v;
2719
		}
2720
	}
2721
}
2722
2723
/**
2724
 * Handles the "ignored boards" section of the profile (if enabled)
2725
 *
2726
 * @param int $memID The ID of the member
2727
 */
2728
function ignoreboards($memID)
2729
{
2730
	global $context, $modSettings, $smcFunc, $cur_profile, $sourcedir;
2731
2732
	// Have the admins enabled this option?
2733
	if (empty($modSettings['allow_ignore_boards']))
2734
		fatal_lang_error('ignoreboards_disallowed', 'user');
2735
2736
	// Find all the boards this user is allowed to see.
2737
	$request = $smcFunc['db_query']('order_by_board_order', '
2738
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
2739
			'. (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
2740
		FROM {db_prefix}boards AS b
2741
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2742
		WHERE {query_see_board}
2743
			AND redirect = {string:empty_string}',
2744
		array(
2745
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
2746
			'empty_string' => '',
2747
		)
2748
	);
2749
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
2750
	$context['categories'] = array();
2751 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2752
	{
2753
		// This category hasn't been set up yet..
2754
		if (!isset($context['categories'][$row['id_cat']]))
2755
			$context['categories'][$row['id_cat']] = array(
2756
				'id' => $row['id_cat'],
2757
				'name' => $row['cat_name'],
2758
				'boards' => array()
2759
			);
2760
2761
		// Set this board up, and let the template know when it's a child.  (indent them..)
2762
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
2763
			'id' => $row['id_board'],
2764
			'name' => $row['name'],
2765
			'child_level' => $row['child_level'],
2766
			'selected' => $row['is_ignored'],
2767
		);
2768
	}
2769
	$smcFunc['db_free_result']($request);
2770
2771
	require_once($sourcedir . '/Subs-Boards.php');
2772
	sortCategories($context['categories']);
2773
2774
	// Now, let's sort the list of categories into the boards for templates that like that.
2775
	$temp_boards = array();
2776 View Code Duplication
	foreach ($context['categories'] as $category)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2777
	{
2778
		// Include a list of boards per category for easy toggling.
2779
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
2780
2781
		$temp_boards[] = array(
2782
			'name' => $category['name'],
2783
			'child_ids' => array_keys($category['boards'])
2784
		);
2785
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
2786
	}
2787
2788
	$max_boards = ceil(count($temp_boards) / 2);
2789
	if ($max_boards == 1)
2790
		$max_boards = 2;
2791
2792
	// Now, alternate them so they can be shown left and right ;).
2793
	$context['board_columns'] = array();
2794 View Code Duplication
	for ($i = 0; $i < $max_boards; $i++)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2795
	{
2796
		$context['board_columns'][] = $temp_boards[$i];
2797
		if (isset($temp_boards[$i + $max_boards]))
2798
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
2799
		else
2800
			$context['board_columns'][] = array();
2801
	}
2802
2803
	loadThemeOptions($memID);
2804
}
2805
2806
/**
2807
 * Load all the languages for the profile
2808
 * .
2809
 * @return bool Whether or not the forum has multiple languages installed
2810
 */
2811
function profileLoadLanguages()
2812
{
2813
	global $context;
2814
2815
	$context['profile_languages'] = array();
2816
2817
	// Get our languages!
2818
	getLanguages();
2819
2820
	// Setup our languages.
2821
	foreach ($context['languages'] as $lang)
2822
	{
2823
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
2824
	}
2825
	ksort($context['profile_languages']);
2826
2827
	// Return whether we should proceed with this.
2828
	return count($context['profile_languages']) > 1 ? true : false;
2829
}
2830
2831
/**
2832
 * Handles the "manage groups" section of the profile
2833
 *
2834
 * @return true Always returns true
2835
 */
2836
function profileLoadGroups()
2837
{
2838
	global $cur_profile, $txt, $context, $smcFunc, $user_settings;
2839
2840
	$context['member_groups'] = array(
2841
		0 => array(
2842
			'id' => 0,
2843
			'name' => $txt['no_primary_membergroup'],
2844
			'is_primary' => $cur_profile['id_group'] == 0,
2845
			'can_be_additional' => false,
2846
			'can_be_primary' => true,
2847
		)
2848
	);
2849
	$curGroups = explode(',', $cur_profile['additional_groups']);
2850
2851
	// Load membergroups, but only those groups the user can assign.
2852
	$request = $smcFunc['db_query']('', '
2853
		SELECT group_name, id_group, hidden
2854
		FROM {db_prefix}membergroups
2855
		WHERE id_group != {int:moderator_group}
2856
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
2857
			AND group_type != {int:is_protected}') . '
2858
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
2859
		array(
2860
			'moderator_group' => 3,
2861
			'min_posts' => -1,
2862
			'is_protected' => 1,
2863
			'newbie_group' => 4,
2864
		)
2865
	);
2866
	while ($row = $smcFunc['db_fetch_assoc']($request))
2867
	{
2868
		// We should skip the administrator group if they don't have the admin_forum permission!
2869
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
2870
			continue;
2871
2872
		$context['member_groups'][$row['id_group']] = array(
2873
			'id' => $row['id_group'],
2874
			'name' => $row['group_name'],
2875
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
2876
			'is_additional' => in_array($row['id_group'], $curGroups),
2877
			'can_be_additional' => true,
2878
			'can_be_primary' => $row['hidden'] != 2,
2879
		);
2880
	}
2881
	$smcFunc['db_free_result']($request);
2882
2883
	$context['member']['group_id'] = $user_settings['id_group'];
2884
2885
	return true;
2886
}
2887
2888
/**
2889
 * Load key signature context data.
2890
 *
2891
 * @return true Always returns true
2892
 */
2893
function profileLoadSignatureData()
2894
{
2895
	global $modSettings, $context, $txt, $cur_profile, $memberContext;
2896
2897
	// Signature limits.
2898
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
2899
	$sig_limits = explode(',', $sig_limits);
2900
2901
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
2902
	$context['signature_limits'] = array(
2903
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
2904
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
2905
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
2906
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
2907
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
2908
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
2909
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
2910
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
2911
	);
2912
	// Kept this line in for backwards compatibility!
2913
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
2914
	// Warning message for signature image limits?
2915
	$context['signature_warning'] = '';
2916
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
2917
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
2918
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
2919
		$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']);
2920
2921
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_charset'] == 'UTF-8' || function_exists('iconv'))));
2922
2923
	if (empty($context['do_preview']))
2924
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
2925
	else
2926
	{
2927
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
2928
		$validation = profileValidateSignature($signature);
2929
		if (empty($context['post_errors']))
2930
		{
2931
			loadLanguage('Errors');
2932
			$context['post_errors'] = array();
2933
		}
2934
		$context['post_errors'][] = 'signature_not_yet_saved';
2935
		if ($validation !== true && $validation !== false)
2936
			$context['post_errors'][] = $validation;
2937
2938
		censorText($context['member']['signature']);
2939
		$context['member']['current_signature'] = $context['member']['signature'];
2940
		censorText($signature);
2941
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']]);
2942
		$context['member']['signature'] = $_POST['signature'];
2943
	}
2944
2945
	// Load the spell checker?
2946
	if ($context['show_spellchecking'])
2947
		loadJavaScriptFile('spellcheck.js', array('defer' => false), 'smf_spellcheck');
2948
2949
	return true;
2950
}
2951
2952
/**
2953
 * Load avatar context data.
2954
 *
2955
 * @return true Always returns true
2956
 */
2957
function profileLoadAvatarData()
2958
{
2959
	global $context, $cur_profile, $modSettings, $scripturl;
2960
2961
	$context['avatar_url'] = $modSettings['avatar_url'];
2962
2963
	// Default context.
2964
	$context['member']['avatar'] += array(
2965
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
2966
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
2967
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2968
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2969
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2970
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']) || !empty($modSettings['gravatarOverride']),
2971
	);
2972
2973
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
2974
	{
2975
		$context['member']['avatar'] += array(
2976
			'choice' => 'gravatar',
2977
			'server_pic' => 'blank.png',
2978
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || !empty($modSettings['gravatarOverride']) ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
2979
		);
2980
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
2981
	}
2982
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
2983
	{
2984
		$context['member']['avatar'] += array(
2985
			'choice' => 'upload',
2986
			'server_pic' => 'blank.png',
2987
			'external' => 'http://'
2988
		);
2989
		$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'];
2990
	}
2991
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
2992
		$context['member']['avatar'] += array(
2993
			'choice' => 'external',
2994
			'server_pic' => 'blank.png',
2995
			'external' => $cur_profile['avatar']
2996
		);
2997
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
2998
		$context['member']['avatar'] += array(
2999
			'choice' => 'server_stored',
3000
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3001
			'external' => 'http://'
3002
		);
3003
	else
3004
		$context['member']['avatar'] += array(
3005
			'choice' => 'none',
3006
			'server_pic' => 'blank.png',
3007
			'external' => 'http://'
3008
		);
3009
3010
	// Get a list of all the avatars.
3011
	if ($context['member']['avatar']['allow_server_stored'])
3012
	{
3013
		$context['avatar_list'] = array();
3014
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3015
	}
3016
	else
3017
		$context['avatars'] = array();
3018
3019
	// Second level selected avatar...
3020
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3021
	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']);
3022
}
3023
3024
/**
3025
 * Save a members group.
3026
 *
3027
 * @param int &$value The ID of the (new) primary group
3028
 * @return true Always returns true
3029
 */
3030
function profileSaveGroups(&$value)
3031
{
3032
	global $profile_vars, $old_profile, $context, $smcFunc, $cur_profile;
3033
3034
	// Do we need to protect some groups?
3035 View Code Duplication
	if (!allowedTo('admin_forum'))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3036
	{
3037
		$request = $smcFunc['db_query']('', '
3038
			SELECT id_group
3039
			FROM {db_prefix}membergroups
3040
			WHERE group_type = {int:is_protected}',
3041
			array(
3042
				'is_protected' => 1,
3043
			)
3044
		);
3045
		$protected_groups = array(1);
3046
		while ($row = $smcFunc['db_fetch_assoc']($request))
3047
			$protected_groups[] = $row['id_group'];
3048
		$smcFunc['db_free_result']($request);
3049
3050
		$protected_groups = array_unique($protected_groups);
3051
	}
3052
3053
	// The account page allows the change of your id_group - but not to a protected group!
3054
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3055
		$value = (int) $value;
3056
	// ... otherwise it's the old group sir.
3057
	else
3058
		$value = $old_profile['id_group'];
3059
3060
	// Find the additional membergroups (if any)
3061
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3062
	{
3063
		$additional_groups = array();
3064
		foreach ($_POST['additional_groups'] as $group_id)
3065
		{
3066
			$group_id = (int) $group_id;
3067
			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...
3068
				$additional_groups[] = $group_id;
3069
		}
3070
3071
		// Put the protected groups back in there if you don't have permission to take them away.
3072
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3073
		foreach ($old_additional_groups as $group_id)
3074
		{
3075
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3076
				$additional_groups[] = $group_id;
3077
		}
3078
3079
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3080
		{
3081
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3082
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3083
		}
3084
	}
3085
3086
	// Too often, people remove delete their own account, or something.
3087
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3088
	{
3089
		$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...
3090
3091
		// If they would no longer be an admin, look for any other...
3092
		if (!$stillAdmin)
3093
		{
3094
			$request = $smcFunc['db_query']('', '
3095
				SELECT id_member
3096
				FROM {db_prefix}members
3097
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3098
					AND id_member != {int:selected_member}
3099
				LIMIT 1',
3100
				array(
3101
					'admin_group' => 1,
3102
					'selected_member' => $context['id_member'],
3103
				)
3104
			);
3105
			list ($another) = $smcFunc['db_fetch_row']($request);
3106
			$smcFunc['db_free_result']($request);
3107
3108
			if (empty($another))
3109
				fatal_lang_error('at_least_one_admin', 'critical');
3110
		}
3111
	}
3112
3113
	// If we are changing group status, update permission cache as necessary.
3114
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3115
	{
3116 View Code Duplication
		if ($context['user']['is_owner'])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3117
			$_SESSION['mc']['time'] = 0;
3118
		else
3119
			updateSettings(array('settings_updated' => time()));
3120
	}
3121
3122
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3123
	call_integration_hook('integrate_profile_profileSaveGroups', array($value, $additional_groups));
3124
3125
	return true;
3126
}
3127
3128
/**
3129
 * The avatar is incredibly complicated, what with the options... and what not.
3130
 * @todo argh, the avatar here. Take this out of here!
3131
 *
3132
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3133
 * @return bool|string False if success (or if memID is empty and password authentication failed), otherwise a string indicating what error occurred
3134
 */
3135
function profileSaveAvatarData(&$value)
3136
{
3137
	global $modSettings, $sourcedir, $smcFunc, $profile_vars, $cur_profile, $context;
3138
3139
	$memID = $context['id_member'];
3140
	if (empty($memID) && !empty($context['password_auth_failed']))
3141
		return false;
3142
3143
	require_once($sourcedir . '/ManageAttachments.php');
3144
3145
	// We're going to put this on a nice custom dir.
3146
	$uploadDir = $modSettings['custom_avatar_dir'];
3147
	$id_folder = 1;
3148
3149
	$downloadedExternalAvatar = false;
3150
	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']))
3151
	{
3152
		if (!is_writable($uploadDir))
3153
			fatal_lang_error('attachments_no_write', 'critical');
3154
3155
		require_once($sourcedir . '/Subs-Package.php');
3156
3157
		$url = parse_url($_POST['userpicpersonal']);
3158
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3159
3160
		$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...
3161
		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...
3162
		{
3163
			fwrite($tmpAvatar, $contents);
3164
			fclose($tmpAvatar);
3165
3166
			$downloadedExternalAvatar = true;
3167
			$_FILES['attachment']['tmp_name'] = $new_filename;
3168
		}
3169
	}
3170
3171
	// Removes whatever attachment there was before updating
3172
	if ($value == 'none')
3173
	{
3174
		$profile_vars['avatar'] = '';
3175
3176
		// Reset the attach ID.
3177
		$cur_profile['id_attach'] = 0;
3178
		$cur_profile['attachment_type'] = 0;
3179
		$cur_profile['filename'] = '';
3180
3181
		removeAttachments(array('id_member' => $memID));
3182
	}
3183
3184
	// An avatar from the server-stored galleries.
3185
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3186
	{
3187
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3188
		$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']) : '';
3189
3190
		// Clear current profile...
3191
		$cur_profile['id_attach'] = 0;
3192
		$cur_profile['attachment_type'] = 0;
3193
		$cur_profile['filename'] = '';
3194
3195
		// Get rid of their old avatar. (if uploaded.)
3196
		removeAttachments(array('id_member' => $memID));
3197
	}
3198
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3199
	{
3200
		// 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.
3201
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3202
			$profile_vars['avatar'] = 'gravatar://';
3203
		else
3204
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3205
3206
		// Get rid of their old avatar. (if uploaded.)
3207
		removeAttachments(array('id_member' => $memID));
3208
	}
3209
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3210
	{
3211
		// We need these clean...
3212
		$cur_profile['id_attach'] = 0;
3213
		$cur_profile['attachment_type'] = 0;
3214
		$cur_profile['filename'] = '';
3215
3216
		// Remove any attached avatar...
3217
		removeAttachments(array('id_member' => $memID));
3218
3219
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3220
3221
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3222
			$profile_vars['avatar'] = '';
3223
		// Trying to make us do something we'll regret?
3224
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3225
			return 'bad_avatar_invalid_url';
3226
		// Should we check dimensions?
3227
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3228
		{
3229
			// Now let's validate the avatar.
3230
			$sizes = url_image_size($profile_vars['avatar']);
3231
3232
			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']))))
3233
			{
3234
				// Houston, we have a problem. The avatar is too large!!
3235
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3236
					return 'bad_avatar_too_large';
3237
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3238
				{
3239
					// @todo remove this if appropriate
3240
					require_once($sourcedir . '/Subs-Graphics.php');
3241
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3242
					{
3243
						$profile_vars['avatar'] = '';
3244
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3245
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3246
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3247
					}
3248
					else
3249
						return 'bad_avatar';
3250
				}
3251
			}
3252
		}
3253
	}
3254
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3255
	{
3256
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3257
		{
3258
			// Get the dimensions of the image.
3259
			if (!$downloadedExternalAvatar)
3260
			{
3261
				if (!is_writable($uploadDir))
3262
					fatal_lang_error('attachments_no_write', 'critical');
3263
3264
				$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...
3265
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3266
					fatal_lang_error('attach_timeout', 'critical');
3267
3268
				$_FILES['attachment']['tmp_name'] = $new_filename;
3269
			}
3270
3271
			$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3272
3273
			// No size, then it's probably not a valid pic.
3274
			if ($sizes === false)
3275
			{
3276
				@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...
3277
				return 'bad_avatar';
3278
			}
3279
			// Check whether the image is too large.
3280
			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']))
3281
			{
3282
				if (!empty($modSettings['avatar_resize_upload']))
3283
				{
3284
					// Attempt to chmod it.
3285
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3286
3287
					// @todo remove this require when appropriate
3288
					require_once($sourcedir . '/Subs-Graphics.php');
3289
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3290
					{
3291
						@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...
3292
						return 'bad_avatar';
3293
					}
3294
3295
					// Reset attachment avatar data.
3296
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3297
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3298
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3299
				}
3300
3301
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3302
				else
3303
				{
3304
					@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...
3305
					return 'bad_avatar_too_large';
3306
				}
3307
			}
3308
3309
			// So far, so good, checks lies ahead!
3310
			elseif (is_array($sizes))
3311
			{
3312
				// Now try to find an infection.
3313
				require_once($sourcedir . '/Subs-Graphics.php');
3314
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3315
				{
3316
					// It's bad. Try to re-encode the contents?
3317
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3318
					{
3319
						@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...
3320
						return 'bad_avatar_fail_reencode';
3321
					}
3322
					// We were successful. However, at what price?
3323
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3324
					// Hard to believe this would happen, but can you bet?
3325
					if ($sizes === false)
3326
					{
3327
						@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...
3328
						return 'bad_avatar';
3329
					}
3330
				}
3331
3332
				$extensions = array(
3333
					'1' => 'gif',
3334
					'2' => 'jpg',
3335
					'3' => 'png',
3336
					'6' => 'bmp'
3337
				);
3338
3339
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3340
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3341
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3342
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3343
				$file_hash = '';
3344
3345
				// Remove previous attachments this member might have had.
3346
				removeAttachments(array('id_member' => $memID));
3347
3348
				$smcFunc['db_insert']('',
3349
					'{db_prefix}attachments',
3350
					array(
3351
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3352
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3353
					),
3354
					array(
3355
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3356
						(int) $width, (int) $height, $mime_type, $id_folder,
3357
					),
3358
					array('id_attach')
3359
				);
3360
3361
				$cur_profile['id_attach'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
3362
				$cur_profile['filename'] = $destName;
3363
				$cur_profile['attachment_type'] = 1;
3364
3365
				$destinationPath = $uploadDir . '/' . (empty($file_hash) ? $destName : $cur_profile['id_attach'] . '_' . $file_hash . '.dat');
3366
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3367
				{
3368
					// I guess a man can try.
3369
					removeAttachments(array('id_member' => $memID));
3370
					fatal_lang_error('attach_timeout', 'critical');
3371
				}
3372
3373
				// Attempt to chmod it.
3374
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3375
			}
3376
			$profile_vars['avatar'] = '';
3377
3378
			// Delete any temporary file.
3379
			if (file_exists($_FILES['attachment']['tmp_name']))
3380
				@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...
3381
		}
3382
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3383
		else
3384
			$profile_vars['avatar'] = '';
3385
	}
3386
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3387
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3388
	else
3389
		$profile_vars['avatar'] = '';
3390
3391
	// Setup the profile variables so it shows things right on display!
3392
	$cur_profile['avatar'] = $profile_vars['avatar'];
3393
3394
	return false;
3395
}
3396
3397
/**
3398
 * Validate the signature
3399
 *
3400
 * @param string &$value The new signature
3401
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3402
 */
3403
function profileValidateSignature(&$value)
3404
{
3405
	global $sourcedir, $modSettings, $smcFunc, $txt;
3406
3407
	require_once($sourcedir . '/Subs-Post.php');
3408
3409
	// Admins can do whatever they hell they want!
3410
	if (!allowedTo('admin_forum'))
3411
	{
3412
		// Load all the signature limits.
3413
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3414
		$sig_limits = explode(',', $sig_limits);
3415
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3416
3417
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3418
3419
		// Too many lines?
3420
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3421
		{
3422
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3423
			return 'signature_max_lines';
3424
		}
3425
3426
		// Too many images?!
3427 View Code Duplication
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3428
		{
3429
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3430
			return 'signature_max_image_count';
3431
		}
3432
3433
		// What about too many smileys!
3434
		$smiley_parsed = $unparsed_signature;
3435
		parsesmileys($smiley_parsed);
3436
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3437
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3438
			return 'signature_allow_smileys';
3439
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3440
		{
3441
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3442
			return 'signature_max_smileys';
3443
		}
3444
3445
		// Maybe we are abusing font sizes?
3446
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3447
		{
3448
			foreach ($matches[1] as $ind => $size)
3449
			{
3450
				$limit_broke = 0;
3451
				// Attempt to allow all sizes of abuse, so to speak.
3452 View Code Duplication
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3453
					$limit_broke = $sig_limits[7] . 'px';
3454
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3455
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3456
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3457
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3458
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3459
					$limit_broke = 'large';
3460
3461
				if ($limit_broke)
3462
				{
3463
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3464
					return 'signature_max_font_size';
3465
				}
3466
			}
3467
		}
3468
3469
		// The difficult one - image sizes! Don't error on this - just fix it.
3470
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3471
		{
3472
			// Get all BBC tags...
3473
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3474
			// ... and all HTML ones.
3475
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3476
			// And stick the HTML in the BBC.
3477 View Code Duplication
			if (!empty($matches2))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3478
			{
3479
				foreach ($matches2[0] as $ind => $dummy)
3480
				{
3481
					$matches[0][] = $matches2[0][$ind];
3482
					$matches[1][] = '';
3483
					$matches[2][] = '';
3484
					$matches[3][] = '';
3485
					$matches[4][] = '';
3486
					$matches[5][] = '';
3487
					$matches[6][] = '';
3488
					$matches[7][] = $matches2[1][$ind];
3489
				}
3490
			}
3491
3492
			$replaces = array();
3493
			// Try to find all the images!
3494
			if (!empty($matches))
3495
			{
3496
				foreach ($matches[0] as $key => $image)
3497
				{
3498
					$width = -1; $height = -1;
3499
3500
					// Does it have predefined restraints? Width first.
3501 View Code Duplication
					if ($matches[6][$key])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3502
						$matches[2][$key] = $matches[6][$key];
3503 View Code Duplication
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3504
					{
3505
						$width = $sig_limits[5];
3506
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3507
					}
3508
					elseif ($matches[2][$key])
3509
						$width = $matches[2][$key];
3510
					// ... and height.
3511 View Code Duplication
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3512
					{
3513
						$height = $sig_limits[6];
3514
						if ($width != -1)
3515
							$width = $width * ($height / $matches[4][$key]);
3516
					}
3517
					elseif ($matches[4][$key])
3518
						$height = $matches[4][$key];
3519
3520
					// If the dimensions are still not fixed - we need to check the actual image.
3521 View Code Duplication
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3522
					{
3523
						$sizes = url_image_size($matches[7][$key]);
3524
						if (is_array($sizes))
3525
						{
3526
							// Too wide?
3527
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3528
							{
3529
								$width = $sig_limits[5];
3530
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3531
							}
3532
							// Too high?
3533
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3534
							{
3535
								$height = $sig_limits[6];
3536
								if ($width == -1)
3537
									$width = $sizes[0];
3538
								$width = $width * ($height / $sizes[1]);
3539
							}
3540
							elseif ($width != -1)
3541
								$height = $sizes[1];
3542
						}
3543
					}
3544
3545
					// Did we come up with some changes? If so remake the string.
3546 View Code Duplication
					if ($width != -1 || $height != -1)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3547
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3548
				}
3549
				if (!empty($replaces))
3550
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3551
			}
3552
		}
3553
3554
		// Any disabled BBC?
3555
		$disabledSigBBC = implode('|', $disabledTags);
3556
		if (!empty($disabledSigBBC))
3557
		{
3558
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3559
			{
3560
				$disabledTags = array_unique($disabledTags);
3561
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3562
				return 'signature_disabled_bbc';
3563
			}
3564
		}
3565
	}
3566
3567
	preparsecode($value);
3568
3569
	// Too long?
3570
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3571
	{
3572
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3573
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3574
		return 'signature_max_length';
3575
	}
3576
3577
	return true;
3578
}
3579
3580
/**
3581
 * Validate an email address.
3582
 *
3583
 * @param string $email The email address to validate
3584
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3585
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3586
 */
3587
function profileValidateEmail($email, $memID = 0)
3588
{
3589
	global $smcFunc;
3590
3591
	$email = strtr($email, array('&#039;' => '\''));
3592
3593
	// Check the name and email for validity.
3594
	if (trim($email) == '')
3595
		return 'no_email';
3596
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3597
		return 'bad_email';
3598
3599
	// Email addresses should be and stay unique.
3600
	$request = $smcFunc['db_query']('', '
3601
		SELECT id_member
3602
		FROM {db_prefix}members
3603
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3604
			email_address = {string:email_address}
3605
		LIMIT 1',
3606
		array(
3607
			'selected_member' => $memID,
3608
			'email_address' => $email,
3609
		)
3610
	);
3611
3612
	if ($smcFunc['db_num_rows']($request) > 0)
3613
		return 'email_taken';
3614
	$smcFunc['db_free_result']($request);
3615
3616
	return true;
3617
}
3618
3619
/**
3620
 * Reload a user's settings.
3621
 */
3622
function profileReloadUser()
3623
{
3624
	global $modSettings, $context, $cur_profile;
3625
3626
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3627
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3628
3629
	loadUserSettings();
3630
	writeLog();
3631
}
3632
3633
/**
3634
 * Send the user a new activation email if they need to reactivate!
3635
 */
3636
function profileSendActivation()
3637
{
3638
	global $sourcedir, $profile_vars, $context, $scripturl, $smcFunc, $cookiename, $cur_profile, $language, $modSettings;
3639
3640
	require_once($sourcedir . '/Subs-Post.php');
3641
3642
	// Shouldn't happen but just in case.
3643
	if (empty($profile_vars['email_address']))
3644
		return;
3645
3646
	$replacements = array(
3647
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3648
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3649
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3650
	);
3651
3652
	// Send off the email.
3653
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3654
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3655
3656
	// Log the user out.
3657
	$smcFunc['db_query']('', '
3658
		DELETE FROM {db_prefix}log_online
3659
		WHERE id_member = {int:selected_member}',
3660
		array(
3661
			'selected_member' => $context['id_member'],
3662
		)
3663
	);
3664
	$_SESSION['log_time'] = 0;
3665
	$_SESSION['login_' . $cookiename] = json_encode(array(0, '', 0));
3666
3667
	if (isset($_COOKIE[$cookiename]))
3668
		$_COOKIE[$cookiename] = '';
3669
3670
	loadUserSettings();
3671
3672
	$context['user']['is_logged'] = false;
3673
	$context['user']['is_guest'] = true;
3674
3675
	redirectexit('action=sendactivation');
3676
}
3677
3678
/**
3679
 * Function to allow the user to choose group membership etc...
3680
 *
3681
 * @param int $memID The ID of the member
3682
 */
3683
function groupMembership($memID)
3684
{
3685
	global $txt, $user_profile, $context, $smcFunc;
3686
3687
	$curMember = $user_profile[$memID];
3688
	$context['primary_group'] = $curMember['id_group'];
3689
3690
	// Can they manage groups?
3691
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3692
	$context['can_manage_protected'] = allowedTo('admin_forum');
3693
	$context['can_edit_primary'] = $context['can_manage_protected'];
3694
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
3695
3696
	// Get all the groups this user is a member of.
3697
	$groups = explode(',', $curMember['additional_groups']);
3698
	$groups[] = $curMember['id_group'];
3699
3700
	// Ensure the query doesn't croak!
3701
	if (empty($groups))
3702
		$groups = array(0);
3703
	// Just to be sure...
3704
	foreach ($groups as $k => $v)
3705
		$groups[$k] = (int) $v;
3706
3707
	// Get all the membergroups they can join.
3708
	$request = $smcFunc['db_query']('', '
3709
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
3710
			COALESCE(lgr.id_member, 0) AS pending
3711
		FROM {db_prefix}membergroups AS mg
3712
			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})
3713
		WHERE (mg.id_group IN ({array_int:group_list})
3714
			OR mg.group_type > {int:nonjoin_group_id})
3715
			AND mg.min_posts = {int:min_posts}
3716
			AND mg.id_group != {int:moderator_group}
3717
		ORDER BY group_name',
3718
		array(
3719
			'group_list' => $groups,
3720
			'selected_member' => $memID,
3721
			'status_open' => 0,
3722
			'nonjoin_group_id' => 1,
3723
			'min_posts' => -1,
3724
			'moderator_group' => 3,
3725
		)
3726
	);
3727
	// This beast will be our group holder.
3728
	$context['groups'] = array(
3729
		'member' => array(),
3730
		'available' => array()
3731
	);
3732
	while ($row = $smcFunc['db_fetch_assoc']($request))
3733
	{
3734
		// Can they edit their primary group?
3735
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
3736
			$context['can_edit_primary'] = true;
3737
3738
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
3739
		if (((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0)) && $row['id_group'] != $context['primary_group'])
3740
			continue;
3741
3742
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
3743
			'id' => $row['id_group'],
3744
			'name' => $row['group_name'],
3745
			'desc' => $row['description'],
3746
			'color' => $row['online_color'],
3747
			'type' => $row['group_type'],
3748
			'pending' => $row['pending'],
3749
			'is_primary' => $row['id_group'] == $context['primary_group'],
3750
			'can_be_primary' => $row['hidden'] != 2,
3751
			// Anything more than this needs to be done through account settings for security.
3752
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
3753
		);
3754
	}
3755
	$smcFunc['db_free_result']($request);
3756
3757
	// Add registered members on the end.
3758
	$context['groups']['member'][0] = array(
3759
		'id' => 0,
3760
		'name' => $txt['regular_members'],
3761
		'desc' => $txt['regular_members_desc'],
3762
		'type' => 0,
3763
		'is_primary' => $context['primary_group'] == 0 ? true : false,
3764
		'can_be_primary' => true,
3765
		'can_leave' => 0,
3766
	);
3767
3768
	// No changing primary one unless you have enough groups!
3769
	if (count($context['groups']['member']) < 2)
3770
		$context['can_edit_primary'] = false;
3771
3772
	// In the special case that someone is requesting membership of a group, setup some special context vars.
3773
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
3774
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
3775
}
3776
3777
/**
3778
 * This function actually makes all the group changes
3779
 *
3780
 * @param array $profile_vars The profile variables
3781
 * @param array $post_errors Any errors that have occurred
3782
 * @param int $memID The ID of the member
3783
 * @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
3784
 */
3785
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...
3786
{
3787
	global $user_info, $context, $user_profile, $modSettings, $smcFunc;
3788
3789
	// Let's be extra cautious...
3790
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
3791
		isAllowedTo('manage_membergroups');
3792
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
3793
		fatal_lang_error('no_access', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
3794
3795
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
3796
3797
	$old_profile = &$user_profile[$memID];
3798
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3799
	$context['can_manage_protected'] = allowedTo('admin_forum');
3800
3801
	// By default the new primary is the old one.
3802
	$newPrimary = $old_profile['id_group'];
3803
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
3804
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
3805
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
3806
3807
	// One way or another, we have a target group in mind...
3808
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
3809
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
3810
3811
	// Sanity check!!
3812
	if ($group_id == 1)
3813
		isAllowedTo('admin_forum');
3814
	// Protected groups too!
3815
	else
3816
	{
3817
		$request = $smcFunc['db_query']('', '
3818
			SELECT group_type
3819
			FROM {db_prefix}membergroups
3820
			WHERE id_group = {int:current_group}
3821
			LIMIT {int:limit}',
3822
			array(
3823
				'current_group' => $group_id,
3824
				'limit' => 1,
3825
			)
3826
		);
3827
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
3828
		$smcFunc['db_free_result']($request);
3829
3830
		if ($is_protected == 1)
3831
			isAllowedTo('admin_forum');
3832
	}
3833
3834
	// What ever we are doing, we need to determine if changing primary is possible!
3835
	$request = $smcFunc['db_query']('', '
3836
		SELECT id_group, group_type, hidden, group_name
3837
		FROM {db_prefix}membergroups
3838
		WHERE id_group IN ({int:group_list}, {int:current_group})',
3839
		array(
3840
			'group_list' => $group_id,
3841
			'current_group' => $old_profile['id_group'],
3842
		)
3843
	);
3844
	while ($row = $smcFunc['db_fetch_assoc']($request))
3845
	{
3846
		// Is this the new group?
3847
		if ($row['id_group'] == $group_id)
3848
		{
3849
			$foundTarget = true;
3850
			$group_name = $row['group_name'];
3851
3852
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
3853
			if ($changeType == 'request' && $row['group_type'] != 2)
3854
				fatal_lang_error('no_access', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
3855
			// What about leaving a requestable group we are not a member of?
3856
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
3857
				fatal_lang_error('no_access', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
3858
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
3859
				fatal_lang_error('no_access', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
3860
3861
			// We can't change the primary group if this is hidden!
3862
			if ($row['hidden'] == 2)
3863
				$canChangePrimary = false;
3864
		}
3865
3866
		// If this is their old primary, can we change it?
3867 View Code Duplication
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3868
			$canChangePrimary = 1;
3869
3870
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
3871
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
3872
			$canChangePrimary = false;
3873
3874
		// If this is the one we are acting on, can we even act?
3875 View Code Duplication
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3876
			$canChangePrimary = false;
3877
	}
3878
	$smcFunc['db_free_result']($request);
3879
3880
	// Didn't find the target?
3881
	if (!$foundTarget)
3882
		fatal_lang_error('no_access', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

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...
3883
3884
	// Final security check, don't allow users to promote themselves to admin.
3885
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
3886
	{
3887
		$request = $smcFunc['db_query']('', '
3888
			SELECT COUNT(permission)
3889
			FROM {db_prefix}permissions
3890
			WHERE id_group = {int:selected_group}
3891
				AND permission = {string:admin_forum}
3892
				AND add_deny = {int:not_denied}',
3893
			array(
3894
				'selected_group' => $group_id,
3895
				'not_denied' => 1,
3896
				'admin_forum' => 'admin_forum',
3897
			)
3898
		);
3899
		list ($disallow) = $smcFunc['db_fetch_row']($request);
3900
		$smcFunc['db_free_result']($request);
3901
3902
		if ($disallow)
3903
			isAllowedTo('admin_forum');
3904
	}
3905
3906
	// If we're requesting, add the note then return.
3907
	if ($changeType == 'request')
3908
	{
3909
		$request = $smcFunc['db_query']('', '
3910
			SELECT id_member
3911
			FROM {db_prefix}log_group_requests
3912
			WHERE id_member = {int:selected_member}
3913
				AND id_group = {int:selected_group}
3914
				AND status = {int:status_open}',
3915
			array(
3916
				'selected_member' => $memID,
3917
				'selected_group' => $group_id,
3918
				'status_open' => 0,
3919
			)
3920
		);
3921
		if ($smcFunc['db_num_rows']($request) != 0)
3922
			fatal_lang_error('profile_error_already_requested_group');
3923
		$smcFunc['db_free_result']($request);
3924
3925
		// Log the request.
3926
		$smcFunc['db_insert']('',
3927
			'{db_prefix}log_group_requests',
3928
			array(
3929
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
3930
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
3931
			),
3932
			array(
3933
				$memID, $group_id, time(), $_POST['reason'],
3934
				0, 0, '', 0, '',
3935
			),
3936
			array('id_request')
3937
		);
3938
3939
		// Set up some data for our background task...
3940
		$data = 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...
3941
3942
		// Add a background task to handle notifying people of this request
3943
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
3944
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
3945
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
3946
		);
3947
3948
		return $changeType;
3949
	}
3950
	// Otherwise we are leaving/joining a group.
3951
	elseif ($changeType == 'free')
3952
	{
3953
		// Are we leaving?
3954
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
3955
		{
3956
			if ($old_profile['id_group'] == $group_id)
3957
				$newPrimary = 0;
3958
			else
3959
				unset($addGroups[$group_id]);
3960
		}
3961
		// ... if not, must be joining.
3962
		else
3963
		{
3964
			// Can we change the primary, and do we want to?
3965
			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...
3966
			{
3967
				if ($old_profile['id_group'] != 0)
3968
					$addGroups[$old_profile['id_group']] = -1;
3969
				$newPrimary = $group_id;
3970
			}
3971
			// Otherwise it's an additional group...
3972
			else
3973
				$addGroups[$group_id] = -1;
3974
		}
3975
	}
3976
	// Finally, we must be setting the primary.
3977
	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...
3978
	{
3979
		if ($old_profile['id_group'] != 0)
3980
			$addGroups[$old_profile['id_group']] = -1;
3981
		if (isset($addGroups[$group_id]))
3982
			unset($addGroups[$group_id]);
3983
		$newPrimary = $group_id;
3984
	}
3985
3986
	// Finally, we can make the changes!
3987
	foreach ($addGroups as $id => $dummy)
3988
		if (empty($id))
3989
			unset($addGroups[$id]);
3990
	$addGroups = implode(',', array_flip($addGroups));
3991
3992
	// Ensure that we don't cache permissions if the group is changing.
3993 View Code Duplication
	if ($context['user']['is_owner'])
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3994
		$_SESSION['mc']['time'] = 0;
3995
	else
3996
		updateSettings(array('settings_updated' => time()));
3997
3998
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
3999
4000
	return $changeType;
4001
}
4002
4003
/**
4004
 * Provides interface to setup Two Factor Auth in SMF
4005
 *
4006
 * @param int $memID The ID of the member
4007
 */
4008
function tfasetup($memID)
4009
{
4010
	global $user_info, $context, $user_settings, $sourcedir, $modSettings;
4011
4012
	require_once($sourcedir . '/Class-TOTP.php');
4013
	require_once($sourcedir . '/Subs-Auth.php');
4014
4015
	// If TFA has not been setup, allow them to set it up
4016
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4017
	{
4018
		// Check to ensure we're forcing SSL for authentication
4019 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
1 ignored issue
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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4020
			fatal_lang_error('login_ssl_required');
4021
4022
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4023
		// we do not want too much AJAX to confuse them.
4024
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4025
		{
4026
			$context['from_ajax'] = true;
4027
			$context['template_layers'] = array();
4028
		}
4029
4030
		// When the code is being sent, verify to make sure the user got it right
4031
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4032
		{
4033
			$code = $_POST['tfa_code'];
4034
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4035
			$totp->setRange(1);
4036
			$valid_password = hash_verify_password($user_settings['member_name'], trim($_POST['passwd']), $user_settings['passwd']);
4037
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4038
4039
			if ($valid_password && $valid_code)
4040
			{
4041
				$backup = substr(sha1(mt_rand()), 0, 16);
4042
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4043
4044
				updateMemberData($memID, array(
4045
					'tfa_secret' => $_SESSION['tfa_secret'],
4046
					'tfa_backup' => $backup_encrypted,
4047
				));
4048
4049
				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 4042 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...
4050
4051
				unset($_SESSION['tfa_secret']);
4052
4053
				$context['tfa_backup'] = $backup;
4054
				$context['sub_template'] = 'tfasetup_backup';
4055
4056
				return;
4057
			}
4058
			else
4059
			{
4060
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4061
				$context['tfa_error'] = !$valid_code;
4062
				$context['tfa_pass_error'] = !$valid_password;
4063
				$context['tfa_pass_value'] = $_POST['passwd'];
4064
				$context['tfa_value'] = $_POST['tfa_code'];
4065
			}
4066
		}
4067
		else
4068
		{
4069
			$totp = new \TOTP\Auth();
4070
			$secret = $totp->generateCode();
4071
			$_SESSION['tfa_secret'] = $secret;
4072
			$context['tfa_secret'] = $secret;
4073
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4074
		}
4075
4076
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4077
	}
4078
	elseif (isset($_REQUEST['disable']))
4079
	{
4080
		updateMemberData($memID, array(
4081
			'tfa_secret' => '',
4082
			'tfa_backup' => '',
4083
		));
4084
		redirectexit('action=profile;area=account;u=' . $memID);
4085
	}
4086
	else
4087
		redirectexit('action=profile;area=account;u=' . $memID);
4088
}
4089
4090
?>
0 ignored issues
show
Best Practice introduced by
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...