Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Sources/Profile-Modify.php (17 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

Code
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 4
16
 */
17
18
if (!defined('SMF'))
19
	die('No direct access...');
20
21
/**
22
 * This defines every profile field known to man.
23
 *
24
 * @param bool $force_reload Whether to reload the data
25
 */
26
function loadProfileFields($force_reload = false)
27
{
28
	global $context, $profile_fields, $txt, $scripturl, $modSettings, $user_info, $smcFunc, $cur_profile, $language;
29
	global $sourcedir, $profile_vars;
30
31
	// Don't load this twice!
32
	if (!empty($profile_fields) && !$force_reload)
33
		return;
34
35
	/* This horrific array defines all the profile fields in the whole world!
36
		In general each "field" has one array - the key of which is the database column name associated with said field. Each item
37
		can have the following attributes:
38
39
				string $type:			The type of field this is - valid types are:
40
					- callback:		This is a field which has its own callback mechanism for templating.
41
					- check:		A simple checkbox.
42
					- hidden:		This doesn't have any visual aspects but may have some validity.
43
					- password:		A password box.
44
					- select:		A select box.
45
					- text:			A string of some description.
46
47
				string $label:			The label for this item - default will be $txt[$key] if this isn't set.
48
				string $subtext:		The subtext (Small label) for this item.
49
				int $size:			Optional size for a text area.
50
				array $input_attr:		An array of text strings to be added to the input box for this item.
51
				string $value:			The value of the item. If not set $cur_profile[$key] is assumed.
52
				string $permission:		Permission required for this item (Excluded _any/_own subfix which is applied automatically).
53
				function $input_validate:	A runtime function which validates the element before going to the database. It is passed
54
								the relevant $_POST element if it exists and should be treated like a reference.
55
56
								Return types:
57
					- true:			Element can be stored.
58
					- false:		Skip this element.
59
					- a text string:	An error occured - this is the error message.
60
61
				function $preload:		A function that is used to load data required for this element to be displayed. Must return
62
								true to be displayed at all.
63
64
				string $cast_type:		If set casts the element to a certain type. Valid types (bool, int, float).
65
				string $save_key:		If the index of this element isn't the database column name it can be overriden
66
								with this string.
67
				bool $is_dummy:			If set then nothing is acted upon for this element.
68
				bool $enabled:			A test to determine whether this is even available - if not is unset.
69
				string $link_with:		Key which links this field to an overall set.
70
71
		Note that all elements that have a custom input_validate must ensure they set the value of $cur_profile correct to enable
72
		the changes to be displayed correctly on submit of the form.
73
74
	*/
75
76
	$profile_fields = array(
77
		'avatar_choice' => array(
78
			'type' => 'callback',
79
			'callback_func' => 'avatar_select',
80
			// This handles the permissions too.
81
			'preload' => 'profileLoadAvatarData',
82
			'input_validate' => 'profileSaveAvatarData',
83
			'save_key' => 'avatar',
84
		),
85
		'bday1' => array(
86
			'type' => 'callback',
87
			'callback_func' => 'birthdate',
88
			'permission' => 'profile_extra',
89
			'preload' => function() use ($cur_profile, &$context)
90
			{
91
				// Split up the birthdate....
92
				list ($uyear, $umonth, $uday) = explode('-', empty($cur_profile['birthdate']) ? '1004-01-01' : $cur_profile['birthdate']);
93
				$context['member']['birth_date'] = array(
94
					'year' => $uyear,
95
					'month' => $umonth,
96
					'day' => $uday,
97
				);
98
99
				return true;
100
			},
101
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
102
			{
103
				if (isset($_POST['bday2'], $_POST['bday3']) && $value > 0 && $_POST['bday2'] > 0)
104
				{
105
					// Set to blank?
106
					if ((int) $_POST['bday3'] == 1 && (int) $_POST['bday2'] == 1 && (int) $value == 1)
107
						$value = '1004-01-01';
108
					else
109
						$value = checkdate($value, $_POST['bday2'], $_POST['bday3'] < 1004 ? 1004 : $_POST['bday3']) ? sprintf('%04d-%02d-%02d', $_POST['bday3'] < 1004 ? 1004 : $_POST['bday3'], $_POST['bday1'], $_POST['bday2']) : '1004-01-01';
110
				}
111
				else
112
					$value = '1004-01-01';
113
114
				$profile_vars['birthdate'] = $value;
115
				$cur_profile['birthdate'] = $value;
116
				return false;
117
			},
118
		),
119
		// Setting the birthdate the old style way?
120
		'birthdate' => array(
121
			'type' => 'hidden',
122
			'permission' => 'profile_extra',
123
			'input_validate' => function(&$value) use ($cur_profile)
124
			{
125
				// @todo Should we check for this year and tell them they made a mistake :P? (based on coppa at least?)
126
				if (preg_match('/(\d{4})[\-\., ](\d{2})[\-\., ](\d{2})/', $value, $dates) === 1)
127
				{
128
					$value = checkdate($dates[2], $dates[3], $dates[1] < 4 ? 4 : $dates[1]) ? sprintf('%04d-%02d-%02d', $dates[1] < 4 ? 4 : $dates[1], $dates[2], $dates[3]) : '1004-01-01';
129
					return true;
130
				}
131
				else
132
				{
133
					$value = empty($cur_profile['birthdate']) ? '1004-01-01' : $cur_profile['birthdate'];
134
					return false;
135
				}
136
			},
137
		),
138
		'date_registered' => array(
139
			'type' => 'date',
140
			'value' => empty($cur_profile['date_registered']) ? $txt['not_applicable'] : strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600),
141
			'label' => $txt['date_registered'],
142
			'log_change' => true,
143
			'permission' => 'moderate_forum',
144
			'input_validate' => function(&$value) use ($txt, $user_info, $modSettings, $cur_profile, $context)
145
			{
146
				// Bad date!  Go try again - please?
147
				if (($value = strtotime($value)) === -1)
148
				{
149
					$value = $cur_profile['date_registered'];
150
					return $txt['invalid_registration'] . ' ' . strftime('%d %b %Y ' . (strpos($user_info['time_format'], '%H') !== false ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
151
				}
152
				// As long as it doesn't equal "N/A"...
153
				elseif ($value != $txt['not_applicable'] && $value != strtotime(strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600)))
154
					$value = $value - ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
155
				else
156
					$value = $cur_profile['date_registered'];
157
158
				return true;
159
			},
160
		),
161
		'email_address' => array(
162
			'type' => 'email',
163
			'label' => $txt['user_email_address'],
164
			'subtext' => $txt['valid_email'],
165
			'log_change' => true,
166
			'permission' => 'profile_password',
167
			'js_submit' => !empty($modSettings['send_validation_onChange']) ? '
168
	form_handle.addEventListener(\'submit\', function(event)
169
	{
170
		if (this.email_address.value != "'. $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
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
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://'))
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)
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)
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);
925
926
		if (!empty($_REQUEST['sa']))
927
			makeCustomFieldChanges($memID, $_REQUEST['sa'], false);
928
929
		foreach ($profile_bools as $var)
930
			if (isset($_POST[$var]))
931
				$profile_vars[$var] = empty($_POST[$var]) ? '0' : '1';
932
		foreach ($profile_ints as $var)
933
			if (isset($_POST[$var]))
934
				$profile_vars[$var] = $_POST[$var] != '' ? (int) $_POST[$var] : '';
935
		foreach ($profile_floats as $var)
936
			if (isset($_POST[$var]))
937
				$profile_vars[$var] = (float) $_POST[$var];
938
		foreach ($profile_strings as $var)
939
			if (isset($_POST[$var]))
940
				$profile_vars[$var] = $_POST[$var];
941
	}
942
}
943
944
/**
945
 * Make any theme changes that are sent with the profile.
946
 *
947
 * @param int $memID The ID of the user
948
 * @param int $id_theme The ID of the theme
949
 */
950
function makeThemeChanges($memID, $id_theme)
951
{
952
	global $modSettings, $smcFunc, $context, $user_info;
953
954
	$reservedVars = array(
955
		'actual_theme_url',
956
		'actual_images_url',
957
		'base_theme_dir',
958
		'base_theme_url',
959
		'default_images_url',
960
		'default_theme_dir',
961
		'default_theme_url',
962
		'default_template',
963
		'images_url',
964
		'number_recent_posts',
965
		'smiley_sets_default',
966
		'theme_dir',
967
		'theme_id',
968
		'theme_layers',
969
		'theme_templates',
970
		'theme_url',
971
	);
972
973
	// Can't change reserved vars.
974
	if ((isset($_POST['options']) && count(array_intersect(array_keys($_POST['options']), $reservedVars)) != 0) || (isset($_POST['default_options']) && count(array_intersect(array_keys($_POST['default_options']), $reservedVars)) != 0))
975
		fatal_lang_error('no_access', false);
976
977
	// Don't allow any overriding of custom fields with default or non-default options.
978
	$request = $smcFunc['db_query']('', '
979
		SELECT col_name
980
		FROM {db_prefix}custom_fields
981
		WHERE active = {int:is_active}',
982
		array(
983
			'is_active' => 1,
984
		)
985
	);
986
	$custom_fields = array();
987
	while ($row = $smcFunc['db_fetch_assoc']($request))
988
		$custom_fields[] = $row['col_name'];
989
	$smcFunc['db_free_result']($request);
990
991
	// These are the theme changes...
992
	$themeSetArray = array();
993
	if (isset($_POST['options']) && is_array($_POST['options']))
994
	{
995
		foreach ($_POST['options'] as $opt => $val)
996
		{
997
			if (in_array($opt, $custom_fields))
998
				continue;
999
1000
			// These need to be controlled.
1001
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1002
				$val = max(0, min($val, 50));
1003
			// We don't set this per theme anymore.
1004
			elseif ($opt == 'allow_no_censored')
1005
				continue;
1006
1007
			$themeSetArray[] = array($memID, $id_theme, $opt, is_array($val) ? implode(',', $val) : $val);
1008
		}
1009
	}
1010
1011
	$erase_options = array();
1012
	if (isset($_POST['default_options']) && is_array($_POST['default_options']))
1013
		foreach ($_POST['default_options'] as $opt => $val)
1014
		{
1015
			if (in_array($opt, $custom_fields))
1016
				continue;
1017
1018
			// These need to be controlled.
1019
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1020
				$val = max(0, min($val, 50));
1021
			// Only let admins and owners change the censor.
1022
			elseif ($opt == 'allow_no_censored' && !$user_info['is_admin'] && !$context['user']['is_owner'])
1023
					continue;
1024
1025
			$themeSetArray[] = array($memID, 1, $opt, is_array($val) ? implode(',', $val) : $val);
1026
			$erase_options[] = $opt;
1027
		}
1028
1029
	// If themeSetArray isn't still empty, send it to the database.
1030
	if (empty($context['password_auth_failed']))
1031
	{
1032 View Code Duplication
		if (!empty($themeSetArray))
1033
		{
1034
			$smcFunc['db_insert']('replace',
1035
				'{db_prefix}themes',
1036
				array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
1037
				$themeSetArray,
1038
				array('id_member', 'id_theme', 'variable')
1039
			);
1040
		}
1041
1042
		if (!empty($erase_options))
1043
		{
1044
			$smcFunc['db_query']('', '
1045
				DELETE FROM {db_prefix}themes
1046
				WHERE id_theme != {int:id_theme}
1047
					AND variable IN ({array_string:erase_variables})
1048
					AND id_member = {int:id_member}',
1049
				array(
1050
					'id_theme' => 1,
1051
					'id_member' => $memID,
1052
					'erase_variables' => $erase_options
1053
				)
1054
			);
1055
		}
1056
1057
		// Admins can choose any theme, even if it's not enabled...
1058
		$themes = allowedTo('admin_forum') ? explode(',', $modSettings['knownThemes']) : explode(',', $modSettings['enableThemes']);
1059
		foreach ($themes as $t)
1060
			cache_put_data('theme_settings-' . $t . ':' . $memID, null, 60);
1061
	}
1062
}
1063
1064
/**
1065
 * Make any notification changes that need to be made.
1066
 *
1067
 * @param int $memID The ID of the member
1068
 */
1069
function makeNotificationChanges($memID)
1070
{
1071
	global $smcFunc, $sourcedir;
1072
1073
	require_once($sourcedir . '/Subs-Notify.php');
1074
1075
	// Update the boards they are being notified on.
1076
	if (isset($_POST['edit_notify_boards']) && !empty($_POST['notify_boards']))
1077
	{
1078
		// Make sure only integers are deleted.
1079
		foreach ($_POST['notify_boards'] as $index => $id)
1080
			$_POST['notify_boards'][$index] = (int) $id;
1081
1082
		// id_board = 0 is reserved for topic notifications.
1083
		$_POST['notify_boards'] = array_diff($_POST['notify_boards'], array(0));
1084
1085
		$smcFunc['db_query']('', '
1086
			DELETE FROM {db_prefix}log_notify
1087
			WHERE id_board IN ({array_int:board_list})
1088
				AND id_member = {int:selected_member}',
1089
			array(
1090
				'board_list' => $_POST['notify_boards'],
1091
				'selected_member' => $memID,
1092
			)
1093
		);
1094
	}
1095
1096
	// We are editing topic notifications......
1097
	elseif (isset($_POST['edit_notify_topics']) && !empty($_POST['notify_topics']))
1098
	{
1099
		foreach ($_POST['notify_topics'] as $index => $id)
1100
			$_POST['notify_topics'][$index] = (int) $id;
1101
1102
		// Make sure there are no zeros left.
1103
		$_POST['notify_topics'] = array_diff($_POST['notify_topics'], array(0));
1104
1105
		$smcFunc['db_query']('', '
1106
			DELETE FROM {db_prefix}log_notify
1107
			WHERE id_topic IN ({array_int:topic_list})
1108
				AND id_member = {int:selected_member}',
1109
			array(
1110
				'topic_list' => $_POST['notify_topics'],
1111
				'selected_member' => $memID,
1112
			)
1113
		);
1114
		foreach ($_POST['notify_topics'] as $topic)
1115
			setNotifyPrefs($memID, array('topic_notify_' . $topic => 0));
1116
	}
1117
1118
	// We are removing topic preferences
1119
	elseif (isset($_POST['remove_notify_topics']) && !empty($_POST['notify_topics']))
1120
	{
1121
		$prefs = array();
1122
		foreach ($_POST['notify_topics'] as $topic)
1123
			$prefs[] = 'topic_notify_' . $topic;
1124
		deleteNotifyPrefs($memID, $prefs);
1125
	}
1126
1127
	// We are removing board preferences
1128
	elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
1129
	{
1130
		$prefs = array();
1131
		foreach ($_POST['notify_boards'] as $board)
1132
			$prefs[] = 'board_notify_' . $board;
1133
		deleteNotifyPrefs($memID, $prefs);
1134
	}
1135
}
1136
1137
/**
1138
 * Save any changes to the custom profile fields
1139
 *
1140
 * @param int $memID The ID of the member
1141
 * @param string $area The area of the profile these fields are in
1142
 * @param bool $sanitize = true Whether or not to sanitize the data
1143
 * @param bool $returnErrors Whether or not to return any error information
1144
 * @return void|array Returns nothing or returns an array of error info if $returnErrors is true
1145
 */
1146
function makeCustomFieldChanges($memID, $area, $sanitize = true, $returnErrors = false)
1147
{
1148
	global $context, $smcFunc, $user_profile, $user_info, $modSettings;
1149
	global $sourcedir;
1150
1151
	$errors = array();
1152
1153
	if ($sanitize && isset($_POST['customfield']))
1154
		$_POST['customfield'] = htmlspecialchars__recursive($_POST['customfield']);
1155
1156
	$where = $area == 'register' ? 'show_reg != 0' : 'show_profile = {string:area}';
1157
1158
	// Load the fields we are saving too - make sure we save valid data (etc).
1159
	$request = $smcFunc['db_query']('', '
1160
		SELECT col_name, field_name, field_desc, field_type, field_length, field_options, default_value, show_reg, mask, private
1161
		FROM {db_prefix}custom_fields
1162
		WHERE ' . $where . '
1163
			AND active = {int:is_active}',
1164
		array(
1165
			'is_active' => 1,
1166
			'area' => $area,
1167
		)
1168
	);
1169
	$changes = array();
1170
	$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);
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)
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))
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();
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))
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)
1476
			{
1477
				// Don't show anything if there isn't anything to show.
1478 View Code Duplication
				if (!isset($context['buddies'][$buddy]['options'][$key]))
1479
				{
1480
					$context['buddies'][$buddy]['options'][$key] = '';
1481
					continue;
1482
				}
1483
1484 View Code Duplication
				if ($column['bbc'] && !empty($context['buddies'][$buddy]['options'][$key]))
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']))
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)
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))
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))
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']))
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
	loadTemplate('Settings');
1798
	loadSubTemplate('options');
1799
1800
	loadThemeOptions($memID);
1801
	if (allowedTo(array('profile_extra_own', 'profile_extra_any')))
1802
		loadCustomFields($memID, 'theme');
1803
1804
	$context['sub_template'] = 'edit_options';
1805
	$context['page_desc'] = $txt['theme_info'];
1806
1807
	setupProfileContext(
1808
		array(
1809
			'id_theme', 'smiley_set', 'hr',
1810
			'time_format', 'timezone', 'hr',
1811
			'theme_settings',
1812
		)
1813
	);
1814
}
1815
1816
/**
1817
 * Display the notifications and settings for changes.
1818
 *
1819
 * @param int $memID The ID of the member
1820
 */
1821
function notification($memID)
1822
{
1823
	global $txt, $context;
1824
1825
	// Going to want this for consistency.
1826
	loadCSSFile('admin.css', array(), 'smf_admin');
1827
1828
	// This is just a bootstrap for everything else.
1829
	$sa = array(
1830
		'alerts' => 'alert_configuration',
1831
		'markread' => 'alert_markread',
1832
		'topics' => 'alert_notifications_topics',
1833
		'boards' => 'alert_notifications_boards',
1834
	);
1835
1836
	$subAction = !empty($_GET['sa']) && isset($sa[$_GET['sa']]) ? $_GET['sa'] : 'alerts';
1837
1838
	$context['sub_template'] = $sa[$subAction];
1839
	$context[$context['profile_menu_name']]['tab_data'] = array(
1840
		'title' => $txt['notification'],
1841
		'help' => '',
1842
		'description' => $txt['notification_info'],
1843
	);
1844
	$sa[$subAction]($memID);
1845
}
1846
1847
/**
1848
 * Handles configuration of alert preferences
1849
 *
1850
 * @param int $memID The ID of the member
1851
 */
1852
function alert_configuration($memID)
1853
{
1854
	global $txt, $context, $modSettings, $smcFunc, $sourcedir;
1855
1856
	if (!isset($context['token_check']))
1857
		$context['token_check'] = 'profile-nt' . $memID;
1858
1859
	is_not_guest();
1860
	if (!$context['user']['is_owner'])
1861
		isAllowedTo('profile_extra_any');
1862
1863
	// Set the post action if we're coming from the profile...
1864
	if (!isset($context['action']))
1865
		$context['action'] = 'action=profile;area=notification;sa=alerts;u=' . $memID;
1866
1867
	// What options are set
1868
	loadThemeOptions($memID);
1869
	loadJavaScriptFile('alertSettings.js', array(), 'smf_alertSettings');
1870
1871
	// Now load all the values for this user.
1872
	require_once($sourcedir . '/Subs-Notify.php');
1873
	$prefs = getNotifyPrefs($memID, '', $memID != 0);
1874
1875
	$context['alert_prefs'] = !empty($prefs[$memID]) ? $prefs[$memID] : array();
1876
1877
	$context['member'] += array(
1878
		'alert_timeout' => isset($context['alert_prefs']['alert_timeout']) ? $context['alert_prefs']['alert_timeout'] : 10,
1879
		'notify_announcements' => isset($context['alert_prefs']['announcements']) ? $context['alert_prefs']['announcements'] : 0,
1880
	);
1881
1882
	// Now for the exciting stuff.
1883
	// We have groups of items, each item has both an alert and an email key as well as an optional help string.
1884
	// Valid values for these keys are 'always', 'yes', 'never'; if using always or never you should add a help string.
1885
	$alert_types = array(
1886
		'board' => array(
1887
			'topic_notify' => array('alert' => 'yes', 'email' => 'yes'),
1888
			'board_notify' => array('alert' => 'yes', 'email' => 'yes'),
1889
		),
1890
		'msg' => array(
1891
			'msg_mention' => array('alert' => 'yes', 'email' => 'yes'),
1892
			'msg_quote' => array('alert' => 'yes', 'email' => 'yes'),
1893
			'msg_like' => array('alert' => 'yes', 'email' => 'never'),
1894
			'unapproved_reply' => array('alert' => 'yes', 'email' => 'yes'),
1895
		),
1896
		'pm' => array(
1897
			'pm_new' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_read', 'is_board' => false)),
1898
			'pm_reply' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_send', 'is_board' => false)),
1899
		),
1900
		'groupr' => array(
1901
			'groupr_approved' => array('alert' => 'always', 'email' => 'yes'),
1902
			'groupr_rejected' => array('alert' => 'always', 'email' => 'yes'),
1903
		),
1904
		'moderation' => array(
1905
			'unapproved_post' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'approve_posts', 'is_board' => true)),
1906
			'msg_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1907
			'msg_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1908
			'member_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1909
			'member_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1910
		),
1911
		'members' => array(
1912
			'member_register' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1913
			'request_group' => array('alert' => 'yes', 'email' => 'yes'),
1914
			'warn_any' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'issue_warning', 'is_board' => false)),
1915
			'buddy_request'  => array('alert' => 'yes', 'email' => 'never'),
1916
			'birthday'  => array('alert' => 'yes', 'email' => 'yes'),
1917
		),
1918
		'calendar' => array(
1919
			'event_new' => array('alert' => 'yes', 'email' => 'yes', 'help' => 'alert_event_new'),
1920
		),
1921
		'paidsubs' => array(
1922
			'paidsubs_expiring' => array('alert' => 'yes', 'email' => 'yes'),
1923
		),
1924
	);
1925
	$group_options = array(
1926
		'board' => array(
1927
			array('check', 'msg_auto_notify', 'label' => 'after'),
1928
			array('check', 'msg_receive_body', 'label' => 'after'),
1929
			array('select', 'msg_notify_pref', 'label' => 'before', 'opts' => array(
1930
				0 => $txt['alert_opt_msg_notify_pref_nothing'],
1931
				1 => $txt['alert_opt_msg_notify_pref_instant'],
1932
				2 => $txt['alert_opt_msg_notify_pref_first'],
1933
				3 => $txt['alert_opt_msg_notify_pref_daily'],
1934
				4 => $txt['alert_opt_msg_notify_pref_weekly'],
1935
			)),
1936
			array('select', 'msg_notify_type', 'label' => 'before', 'opts' => array(
1937
				1 => $txt['notify_send_type_everything'],
1938
				2 => $txt['notify_send_type_everything_own'],
1939
				3 => $txt['notify_send_type_only_replies'],
1940
				4 => $txt['notify_send_type_nothing'],
1941
			)),
1942
		),
1943
		'pm' => array(
1944
			array('select', 'pm_notify', 'label' => 'before', 'opts' => array(
1945
				1 => $txt['email_notify_all'],
1946
				2 => $txt['email_notify_buddies'],
1947
			)),
1948
		),
1949
	);
1950
1951
	// There are certain things that are disabled at the group level.
1952
	if (empty($modSettings['cal_enabled']))
1953
		unset($alert_types['calendar']);
1954
1955
	// Disable paid subscriptions at group level if they're disabled
1956
	if (empty($modSettings['paid_enabled']))
1957
		unset($alert_types['paidsubs']);
1958
1959
	// Disable membergroup requests at group level if they're disabled
1960
	if (empty($modSettings['show_group_membership']))
1961
		unset($alert_types['groupr'], $alert_types['members']['request_group']);
1962
1963
	// Disable mentions if they're disabled
1964
	if (empty($modSettings['enable_mentions']))
1965
		unset($alert_types['msg']['msg_mention']);
1966
1967
	// Disable likes if they're disabled
1968
	if (empty($modSettings['enable_likes']))
1969
		unset($alert_types['msg']['msg_like']);
1970
1971
	// Disable buddy requests if they're disabled
1972
	if (empty($modSettings['enable_buddylist']))
1973
		unset($alert_types['members']['buddy_request']);
1974
1975
	// Now, now, we could pass this through global but we should really get into the habit of
1976
	// passing content to hooks, not expecting hooks to splatter everything everywhere.
1977
	call_integration_hook('integrate_alert_types', array(&$alert_types, &$group_options));
1978
1979
	// Now we have to do some permissions testing - but only if we're not loading this from the admin center
1980
	if (!empty($memID))
1981
	{
1982
		require_once($sourcedir . '/Subs-Members.php');
1983
		$perms_cache = array();
1984
		$request = $smcFunc['db_query']('', '
1985
			SELECT COUNT(*)
1986
			FROM {db_prefix}group_moderators
1987
			WHERE id_member = {int:memID}',
1988
			array(
1989
				'memID' => $memID,
1990
			)
1991
		);
1992
1993
		list ($can_mod) = $smcFunc['db_fetch_row']($request);
1994
1995
		if (!isset($perms_cache['manage_membergroups']))
1996
		{
1997
			$members = membersAllowedTo('manage_membergroups');
1998
			$perms_cache['manage_membergroups'] = in_array($memID, $members);
1999
		}
2000
2001
		if (!($perms_cache['manage_membergroups'] || $can_mod != 0))
2002
			unset($alert_types['members']['request_group']);
2003
2004
		foreach ($alert_types as $group => $items)
2005
		{
2006
			foreach ($items as $alert_key => $alert_value)
2007
			{
2008
				if (!isset($alert_value['permission']))
2009
					continue;
2010
				if (!isset($perms_cache[$alert_value['permission']['name']]))
2011
				{
2012
					$in_board = !empty($alert_value['permission']['is_board']) ? 0 : null;
2013
					$members = membersAllowedTo($alert_value['permission']['name'], $in_board);
2014
					$perms_cache[$alert_value['permission']['name']] = in_array($memID, $members);
2015
				}
2016
2017
				if (!$perms_cache[$alert_value['permission']['name']])
2018
					unset ($alert_types[$group][$alert_key]);
2019
			}
2020
2021
			if (empty($alert_types[$group]))
2022
				unset ($alert_types[$group]);
2023
		}
2024
	}
2025
2026
	// And finally, exporting it to be useful later.
2027
	$context['alert_types'] = $alert_types;
2028
	$context['alert_group_options'] = $group_options;
2029
2030
	$context['alert_bits'] = array(
2031
		'alert' => 0x01,
2032
		'email' => 0x02,
2033
	);
2034
2035
	if (isset($_POST['notify_submit']))
2036
	{
2037
		checkSession();
2038
		validateToken($context['token_check'], 'post');
2039
2040
		// We need to step through the list of valid settings and figure out what the user has set.
2041
		$update_prefs = array();
2042
2043
		// Now the group level options
2044
		foreach ($context['alert_group_options'] as $opt_group => $group)
2045
		{
2046
			foreach ($group as $this_option)
2047
			{
2048
				switch ($this_option[0])
2049
				{
2050
					case 'check':
2051
						$update_prefs[$this_option[1]] = !empty($_POST['opt_' . $this_option[1]]) ? 1 : 0;
2052
						break;
2053
					case 'select':
2054
						if (isset($_POST['opt_' . $this_option[1]], $this_option['opts'][$_POST['opt_' . $this_option[1]]]))
2055
							$update_prefs[$this_option[1]] = $_POST['opt_' . $this_option[1]];
2056
						else
2057
						{
2058
							// We didn't have a sane value. Let's grab the first item from the possibles.
2059
							$keys = array_keys($this_option['opts']);
2060
							$first = array_shift($keys);
2061
							$update_prefs[$this_option[1]] = $first;
2062
						}
2063
						break;
2064
				}
2065
			}
2066
		}
2067
2068
		// Now the individual options
2069
		foreach ($context['alert_types'] as $alert_group => $items)
2070
		{
2071
			foreach ($items as $item_key => $this_options)
2072
			{
2073
				$this_value = 0;
2074
				foreach ($context['alert_bits'] as $type => $bitvalue)
2075
				{
2076
					if ($this_options[$type] == 'yes' && !empty($_POST[$type . '_' . $item_key]) || $this_options[$type] == 'always')
2077
						$this_value |= $bitvalue;
2078
				}
2079
				if (!isset($context['alert_prefs'][$item_key]) || $context['alert_prefs'][$item_key] != $this_value)
2080
					$update_prefs[$item_key] = $this_value;
2081
			}
2082
		}
2083
2084
		if (!empty($_POST['opt_alert_timeout']))
2085
			$update_prefs['alert_timeout'] = $context['member']['alert_timeout'] = (int) $_POST['opt_alert_timeout'];
2086
2087
		if (!empty($_POST['notify_announcements']))
2088
			$update_prefs['announcements'] = $context['member']['notify_announcements'] = (int) $_POST['notify_announcements'];
2089
2090
		setNotifyPrefs((int) $memID, $update_prefs);
2091
		foreach ($update_prefs as $pref => $value)
2092
			$context['alert_prefs'][$pref] = $value;
2093
2094
		makeNotificationChanges($memID);
2095
2096
		$context['profile_updated'] = $txt['profile_updated_own'];
2097
	}
2098
2099
	createToken($context['token_check'], 'post');
2100
}
2101
2102
/**
2103
 * Marks all alerts as read for the specified user
2104
 *
2105
 * @param int $memID The ID of the member
2106
 */
2107
function alert_markread($memID)
2108
{
2109
	global $context, $db_show_debug, $smcFunc;
2110
2111
	// We do not want to output debug information here.
2112
	$db_show_debug = false;
2113
2114
	// We only want to output our little layer here.
2115
	$context['template_layers'] = array();
2116
	$context['sub_template'] = 'alerts_all_read';
2117
2118
	loadLanguage('Alerts');
2119
2120
	// Now we're all set up.
2121
	is_not_guest();
2122
	if (!$context['user']['is_owner'])
2123
		fatal_error('no_access');
2124
2125
	checkSession('get');
2126
2127
	// Assuming we're here, mark everything as read and head back.
2128
	// We only spit back the little layer because this should be called AJAXively.
2129
	$smcFunc['db_query']('', '
2130
		UPDATE {db_prefix}user_alerts
2131
		SET is_read = {int:now}
2132
		WHERE id_member = {int:current_member}
2133
			AND is_read = 0',
2134
		array(
2135
			'now' => time(),
2136
			'current_member' => $memID,
2137
		)
2138
	);
2139
2140
	updateMemberData($memID, array('alerts' => 0));
2141
}
2142
2143
/**
2144
 * Marks a group of alerts as un/read
2145
 *
2146
 * @param int $memID The user ID.
2147
 * @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.
2148
 * @param integer $read To mark as read or unread, 1 for read, 0 or any other value different than 1 for unread.
2149
 * @return integer How many alerts remain unread
2150
 */
2151
function alert_mark($memID, $toMark, $read = 0)
2152
{
2153
	global $smcFunc;
2154
2155
	if (empty($toMark) || empty($memID))
2156
		return false;
2157
2158
	$toMark = (array) $toMark;
2159
2160
	$smcFunc['db_query']('', '
2161
		UPDATE {db_prefix}user_alerts
2162
		SET is_read = {int:read}
2163
		WHERE id_alert IN({array_int:toMark})',
2164
		array(
2165
			'read' => $read == 1 ? time() : 0,
2166
			'toMark' => $toMark,
2167
		)
2168
	);
2169
2170
	// Gotta know how many unread alerts are left.
2171
	$count = alert_count($memID, true);
2172
2173
	updateMemberData($memID, array('alerts' => $count));
2174
2175
	// Might want to know this.
2176
	return $count;
2177
}
2178
2179
/**
2180
 * Deletes a single or a group of alerts by ID
2181
 *
2182
 * @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.
2183
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2184
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
2185
 */
2186
function alert_delete($toDelete, $memID = false)
2187
{
2188
	global $smcFunc;
2189
2190
	if (empty($toDelete))
2191
		return false;
2192
2193
	$toDelete = (array) $toDelete;
2194
2195
	$smcFunc['db_query']('', '
2196
		DELETE FROM {db_prefix}user_alerts
2197
		WHERE id_alert IN({array_int:toDelete})',
2198
		array(
2199
			'toDelete' => $toDelete,
2200
		)
2201
	);
2202
2203
	// Gotta know how many unread alerts are left.
2204
	if ($memID)
2205
	{
2206
		$count = alert_count($memID, true);
0 ignored issues
show
It seems like $memID defined by parameter $memID on line 2186 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...
2207
2208
		updateMemberData($memID, array('alerts' => $count));
2209
2210
		// Might want to know this.
2211
		return $count;
2212
	}
2213
}
2214
2215
/**
2216
 * Counts how many alerts a user has - either unread or all depending on $unread
2217
 *
2218
 * @param int $memID The user ID.
2219
 * @param bool $unread Whether to only count unread alerts.
2220
 * @return int The number of requested alerts
2221
 */
2222
function alert_count($memID, $unread = false)
2223
{
2224
	global $smcFunc;
2225
2226
	if (empty($memID))
2227
		return false;
2228
2229
	$request = $smcFunc['db_query']('', '
2230
		SELECT id_alert
2231
		FROM {db_prefix}user_alerts
2232
		WHERE id_member = {int:id_member}
2233
			'.($unread ? '
2234
			AND is_read = 0' : ''),
2235
		array(
2236
			'id_member' => $memID,
2237
		)
2238
	);
2239
2240
	$count = $smcFunc['db_num_rows']($request);
2241
	$smcFunc['db_free_result']($request);
2242
2243
	return $count;
2244
}
2245
2246
/**
2247
 * Handles alerts related to topics and posts
2248
 *
2249
 * @param int $memID The ID of the member
2250
 */
2251
function alert_notifications_topics($memID)
2252
{
2253
	global $txt, $scripturl, $context, $modSettings, $sourcedir;
2254
2255
	// Because of the way this stuff works, we want to do this ourselves.
2256 View Code Duplication
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
2257
	{
2258
		checkSession();
2259
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2260
2261
		makeNotificationChanges($memID);
2262
		$context['profile_updated'] = $txt['profile_updated_own'];
2263
	}
2264
2265
	// Now set up for the token check.
2266
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2267
	createToken($context['token_check'], 'post');
2268
2269
	// Gonna want this for the list.
2270
	require_once($sourcedir . '/Subs-List.php');
2271
2272
	// Do the topic notifications.
2273
	$listOptions = array(
2274
		'id' => 'topic_notification_list',
2275
		'width' => '100%',
2276
		'items_per_page' => $modSettings['defaultMaxListItems'],
2277
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2278
		'no_items_align' => 'left',
2279
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2280
		'default_sort_col' => 'last_post',
2281
		'get_items' => array(
2282
			'function' => 'list_getTopicNotifications',
2283
			'params' => array(
2284
				$memID,
2285
			),
2286
		),
2287
		'get_count' => array(
2288
			'function' => 'list_getTopicNotificationCount',
2289
			'params' => array(
2290
				$memID,
2291
			),
2292
		),
2293
		'columns' => array(
2294
			'subject' => array(
2295
				'header' => array(
2296
					'value' => $txt['notifications_topics'],
2297
					'class' => 'lefttext',
2298
				),
2299
				'data' => array(
2300
					'function' => function($topic) use ($txt)
2301
					{
2302
						$link = $topic['link'];
2303
2304
						if ($topic['new'])
2305
							$link .= ' <a href="' . $topic['new_href'] . '"><span class="new_posts">' . $txt['new'] . '</span></a>';
2306
2307
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2308
2309
						return $link;
2310
					},
2311
				),
2312
				'sort' => array(
2313
					'default' => 'ms.subject',
2314
					'reverse' => 'ms.subject DESC',
2315
				),
2316
			),
2317
			'started_by' => array(
2318
				'header' => array(
2319
					'value' => $txt['started_by'],
2320
					'class' => 'lefttext',
2321
				),
2322
				'data' => array(
2323
					'db' => 'poster_link',
2324
				),
2325
				'sort' => array(
2326
					'default' => 'real_name_col',
2327
					'reverse' => 'real_name_col DESC',
2328
				),
2329
			),
2330
			'last_post' => array(
2331
				'header' => array(
2332
					'value' => $txt['last_post'],
2333
					'class' => 'lefttext',
2334
				),
2335
				'data' => array(
2336
					'sprintf' => array(
2337
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2338
						'params' => array(
2339
							'updated' => false,
2340
							'poster_updated_link' => false,
2341
						),
2342
					),
2343
				),
2344
				'sort' => array(
2345
					'default' => 'ml.id_msg DESC',
2346
					'reverse' => 'ml.id_msg',
2347
				),
2348
			),
2349
			'alert' => array(
2350
				'header' => array(
2351
					'value' => $txt['notify_what_how'],
2352
					'class' => 'lefttext',
2353
				),
2354
				'data' => array(
2355
					'function' => function($topic) use ($txt)
2356
					{
2357
						$pref = $topic['notify_pref'];
2358
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2359
						return $txt['notify_topic_' . $mode];
2360
					},
2361
				),
2362
			),
2363
			'delete' => array(
2364
				'header' => array(
2365
					'value' => '<input type="checkbox" class="input_check" onclick="invertAll(this, this.form);">',
2366
					'style' => 'width: 4%;',
2367
					'class' => 'centercol',
2368
				),
2369
				'data' => array(
2370
					'sprintf' => array(
2371
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d" class="input_check">',
2372
						'params' => array(
2373
							'id' => false,
2374
						),
2375
					),
2376
					'class' => 'centercol',
2377
				),
2378
			),
2379
		),
2380
		'form' => array(
2381
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2382
			'include_sort' => true,
2383
			'include_start' => true,
2384
			'hidden_fields' => array(
2385
				'u' => $memID,
2386
				'sa' => $context['menu_item_selected'],
2387
				$context['session_var'] => $context['session_id'],
2388
			),
2389
			'token' => $context['token_check'],
2390
		),
2391
		'additional_rows' => array(
2392
			array(
2393
				'position' => 'bottom_of_list',
2394
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button_submit" />
2395
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button_submit" />',
2396
				'class' => 'floatright',
2397
			),
2398
		),
2399
	);
2400
2401
	// Create the notification list.
2402
	createList($listOptions);
2403
}
2404
2405
/**
2406
 * Handles preferences related to board-level notifications
2407
 *
2408
 * @param int $memID The ID of the member
2409
 */
2410
function alert_notifications_boards($memID)
2411
{
2412
	global $txt, $scripturl, $context, $sourcedir;
2413
2414
	// Because of the way this stuff works, we want to do this ourselves.
2415 View Code Duplication
	if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
0 ignored issues
show
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...
2416
	{
2417
		checkSession();
2418
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2419
2420
		makeNotificationChanges($memID);
2421
		$context['profile_updated'] = $txt['profile_updated_own'];
2422
	}
2423
2424
	// Now set up for the token check.
2425
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2426
	createToken($context['token_check'], 'post');
2427
2428
	// Gonna want this for the list.
2429
	require_once($sourcedir . '/Subs-List.php');
2430
2431
	// Fine, start with the board list.
2432
	$listOptions = array(
2433
		'id' => 'board_notification_list',
2434
		'width' => '100%',
2435
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2436
		'no_items_align' => 'left',
2437
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2438
		'default_sort_col' => 'board_name',
2439
		'get_items' => array(
2440
			'function' => 'list_getBoardNotifications',
2441
			'params' => array(
2442
				$memID,
2443
			),
2444
		),
2445
		'columns' => array(
2446
			'board_name' => array(
2447
				'header' => array(
2448
					'value' => $txt['notifications_boards'],
2449
					'class' => 'lefttext',
2450
				),
2451
				'data' => array(
2452
					'function' => function($board) use ($txt)
2453
					{
2454
						$link = $board['link'];
2455
2456
						if ($board['new'])
2457
							$link .= ' <a href="' . $board['href'] . '"><span class="new_posts">' . $txt['new'] . '</span></a>';
2458
2459
						return $link;
2460
					},
2461
				),
2462
				'sort' => array(
2463
					'default' => 'name',
2464
					'reverse' => 'name DESC',
2465
				),
2466
			),
2467
			'alert' => array(
2468
				'header' => array(
2469
					'value' => $txt['notify_what_how'],
2470
					'class' => 'lefttext',
2471
				),
2472
				'data' => array(
2473
					'function' => function($board) use ($txt)
2474
					{
2475
						$pref = $board['notify_pref'];
2476
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2477
						return $txt['notify_board_' . $mode];
2478
					},
2479
				),
2480
			),
2481
			'delete' => array(
2482
				'header' => array(
2483
					'value' => '<input type="checkbox" class="input_check" onclick="invertAll(this, this.form);">',
2484
					'style' => 'width: 4%;',
2485
					'class' => 'centercol',
2486
				),
2487
				'data' => array(
2488
					'sprintf' => array(
2489
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d" class="input_check">',
2490
						'params' => array(
2491
							'id' => false,
2492
						),
2493
					),
2494
					'class' => 'centercol',
2495
				),
2496
			),
2497
		),
2498
		'form' => array(
2499
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2500
			'include_sort' => true,
2501
			'include_start' => true,
2502
			'hidden_fields' => array(
2503
				'u' => $memID,
2504
				'sa' => $context['menu_item_selected'],
2505
				$context['session_var'] => $context['session_id'],
2506
			),
2507
			'token' => $context['token_check'],
2508
		),
2509
		'additional_rows' => array(
2510
			array(
2511
				'position' => 'bottom_of_list',
2512
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button_submit">
2513
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button_submit" />',
2514
				'class' => 'floatright',
2515
			),
2516
		),
2517
	);
2518
2519
	// Create the board notification list.
2520
	createList($listOptions);
2521
}
2522
2523
/**
2524
 * Determins how many topics a user has requested notifications for
2525
 *
2526
 * @param int $memID The ID of the member
2527
 * @return int The number of topic notifications for this user
2528
 */
2529
function list_getTopicNotificationCount($memID)
2530
{
2531
	global $smcFunc, $user_info, $modSettings;
2532
2533
	$request = $smcFunc['db_query']('', '
2534
		SELECT COUNT(*)
2535
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2536
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2537
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2538
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2539
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2540
			AND t.approved = {int:is_approved}' : ''),
2541
		array(
2542
			'selected_member' => $memID,
2543
			'is_approved' => 1,
2544
		)
2545
	);
2546
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2547
	$smcFunc['db_free_result']($request);
2548
2549
	return (int) $totalNotifications;
2550
}
2551
2552
/**
2553
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2554
 *
2555
 * @param int $start Which item to start with (for pagination purposes)
2556
 * @param int $items_per_page How many items to display on each page
2557
 * @param string $sort A string indicating how to sort the results
2558
 * @param int $memID The ID of the member
2559
 * @return array An array of information about the topics a user has subscribed to
2560
 */
2561
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2562
{
2563
	global $smcFunc, $scripturl, $user_info, $modSettings, $sourcedir;
2564
2565
	require_once($sourcedir . '/Subs-Notify.php');
2566
	$prefs = getNotifyPrefs($memID);
2567
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2568
2569
	// All the topics with notification on...
2570
	$request = $smcFunc['db_query']('', '
2571
		SELECT
2572
			COALESCE(lt.id_msg, COALESCE(lmr.id_msg, -1)) + 1 AS new_from, b.id_board, b.name,
2573
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2574
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2575
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2576
			lt.unwatched
2577
		FROM {db_prefix}log_notify AS ln
2578
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2579
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2580
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2581
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2582
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2583
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2584
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2585
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2586
		WHERE ln.id_member = {int:selected_member}
2587
		ORDER BY {raw:sort}
2588
		LIMIT {int:offset}, {int:items_per_page}',
2589
		array(
2590
			'current_member' => $user_info['id'],
2591
			'is_approved' => 1,
2592
			'selected_member' => $memID,
2593
			'sort' => $sort,
2594
			'offset' => $start,
2595
			'items_per_page' => $items_per_page,
2596
		)
2597
	);
2598
	$notification_topics = array();
2599
	while ($row = $smcFunc['db_fetch_assoc']($request))
2600
	{
2601
		censorText($row['subject']);
2602
2603
		$notification_topics[] = array(
2604
			'id' => $row['id_topic'],
2605
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2606
			'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>',
2607
			'subject' => $row['subject'],
2608
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2609
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2610
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2611
			'new_from' => $row['new_from'],
2612
			'updated' => timeformat($row['poster_time']),
2613
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2614
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2615
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2616
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2617
			'unwatched' => $row['unwatched'],
2618
		);
2619
	}
2620
	$smcFunc['db_free_result']($request);
2621
2622
	return $notification_topics;
2623
}
2624
2625
/**
2626
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2627
 *
2628
 * @param int $start Which item to start with (not used here)
2629
 * @param int $items_per_page How many items to show on each page (not used here)
2630
 * @param string $sort A string indicating how to sort the results
2631
 * @param int $memID The ID of the member
2632
 * @return array An array of information about all the boards a user is subscribed to
2633
 */
2634
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2635
{
2636
	global $smcFunc, $scripturl, $user_info, $sourcedir;
2637
2638
	require_once($sourcedir . '/Subs-Notify.php');
2639
	$prefs = getNotifyPrefs($memID);
2640
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2641
2642
	$request = $smcFunc['db_query']('', '
2643
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2644
		FROM {db_prefix}log_notify AS ln
2645
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2646
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2647
		WHERE ln.id_member = {int:selected_member}
2648
			AND {query_see_board}
2649
		ORDER BY {raw:sort}',
2650
		array(
2651
			'current_member' => $user_info['id'],
2652
			'selected_member' => $memID,
2653
			'sort' => $sort,
2654
		)
2655
	);
2656
	$notification_boards = array();
2657
	while ($row = $smcFunc['db_fetch_assoc']($request))
2658
		$notification_boards[] = array(
2659
			'id' => $row['id_board'],
2660
			'name' => $row['name'],
2661
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2662
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2663
			'new' => $row['board_read'] < $row['id_msg_updated'],
2664
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2665
		);
2666
	$smcFunc['db_free_result']($request);
2667
2668
	return $notification_boards;
2669
}
2670
2671
/**
2672
 * Loads the theme options for a user
2673
 *
2674
 * @param int $memID The ID of the member
2675
 */
2676
function loadThemeOptions($memID)
2677
{
2678
	global $context, $options, $cur_profile, $smcFunc;
2679
2680 View Code Duplication
	if (isset($_POST['default_options']))
2681
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2682
2683
	if ($context['user']['is_owner'])
2684
	{
2685
		$context['member']['options'] = $options;
2686
		if (isset($_POST['options']) && is_array($_POST['options']))
2687
			foreach ($_POST['options'] as $k => $v)
2688
				$context['member']['options'][$k] = $v;
2689
	}
2690
	else
2691
	{
2692
		$request = $smcFunc['db_query']('', '
2693
			SELECT id_member, variable, value
2694
			FROM {db_prefix}themes
2695
			WHERE id_theme IN (1, {int:member_theme})
2696
				AND id_member IN (-1, {int:selected_member})',
2697
			array(
2698
				'member_theme' => (int) $cur_profile['id_theme'],
2699
				'selected_member' => $memID,
2700
			)
2701
		);
2702
		$temp = array();
2703
		while ($row = $smcFunc['db_fetch_assoc']($request))
2704
		{
2705
			if ($row['id_member'] == -1)
2706
			{
2707
				$temp[$row['variable']] = $row['value'];
2708
				continue;
2709
			}
2710
2711
			if (isset($_POST['options'][$row['variable']]))
2712
				$row['value'] = $_POST['options'][$row['variable']];
2713
			$context['member']['options'][$row['variable']] = $row['value'];
2714
		}
2715
		$smcFunc['db_free_result']($request);
2716
2717
		// Load up the default theme options for any missing.
2718
		foreach ($temp as $k => $v)
2719
		{
2720
			if (!isset($context['member']['options'][$k]))
2721
				$context['member']['options'][$k] = $v;
2722
		}
2723
	}
2724
}
2725
2726
/**
2727
 * Handles the "ignored boards" section of the profile (if enabled)
2728
 *
2729
 * @param int $memID The ID of the member
2730
 */
2731
function ignoreboards($memID)
2732
{
2733
	global $context, $modSettings, $smcFunc, $cur_profile, $sourcedir;
2734
2735
	// Have the admins enabled this option?
2736
	if (empty($modSettings['allow_ignore_boards']))
2737
		fatal_lang_error('ignoreboards_disallowed', 'user');
2738
2739
	// Find all the boards this user is allowed to see.
2740
	$request = $smcFunc['db_query']('order_by_board_order', '
2741
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
2742
			'. (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
2743
		FROM {db_prefix}boards AS b
2744
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2745
		WHERE {query_see_board}
2746
			AND redirect = {string:empty_string}',
2747
		array(
2748
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
2749
			'empty_string' => '',
2750
		)
2751
	);
2752
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
2753
	$context['categories'] = array();
2754 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2755
	{
2756
		// This category hasn't been set up yet..
2757
		if (!isset($context['categories'][$row['id_cat']]))
2758
			$context['categories'][$row['id_cat']] = array(
2759
				'id' => $row['id_cat'],
2760
				'name' => $row['cat_name'],
2761
				'boards' => array()
2762
			);
2763
2764
		// Set this board up, and let the template know when it's a child.  (indent them..)
2765
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
2766
			'id' => $row['id_board'],
2767
			'name' => $row['name'],
2768
			'child_level' => $row['child_level'],
2769
			'selected' => $row['is_ignored'],
2770
		);
2771
	}
2772
	$smcFunc['db_free_result']($request);
2773
2774
	require_once($sourcedir . '/Subs-Boards.php');
2775
	sortCategories($context['categories']);
2776
2777
	// Now, let's sort the list of categories into the boards for templates that like that.
2778
	$temp_boards = array();
2779 View Code Duplication
	foreach ($context['categories'] as $category)
2780
	{
2781
		// Include a list of boards per category for easy toggling.
2782
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
2783
2784
		$temp_boards[] = array(
2785
			'name' => $category['name'],
2786
			'child_ids' => array_keys($category['boards'])
2787
		);
2788
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
2789
	}
2790
2791
	$max_boards = ceil(count($temp_boards) / 2);
2792
	if ($max_boards == 1)
2793
		$max_boards = 2;
2794
2795
	// Now, alternate them so they can be shown left and right ;).
2796
	$context['board_columns'] = array();
2797 View Code Duplication
	for ($i = 0; $i < $max_boards; $i++)
2798
	{
2799
		$context['board_columns'][] = $temp_boards[$i];
2800
		if (isset($temp_boards[$i + $max_boards]))
2801
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
2802
		else
2803
			$context['board_columns'][] = array();
2804
	}
2805
2806
	loadThemeOptions($memID);
2807
}
2808
2809
/**
2810
 * Load all the languages for the profile
2811
 * .
2812
 * @return bool Whether or not the forum has multiple languages installed
2813
 */
2814
function profileLoadLanguages()
2815
{
2816
	global $context;
2817
2818
	$context['profile_languages'] = array();
2819
2820
	// Get our languages!
2821
	getLanguages();
2822
2823
	// Setup our languages.
2824
	foreach ($context['languages'] as $lang)
2825
	{
2826
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
2827
	}
2828
	ksort($context['profile_languages']);
2829
2830
	// Return whether we should proceed with this.
2831
	return count($context['profile_languages']) > 1 ? true : false;
2832
}
2833
2834
/**
2835
 * Handles the "manage groups" section of the profile
2836
 *
2837
 * @return true Always returns true
2838
 */
2839
function profileLoadGroups()
2840
{
2841
	global $cur_profile, $txt, $context, $smcFunc, $user_settings;
2842
2843
	$context['member_groups'] = array(
2844
		0 => array(
2845
			'id' => 0,
2846
			'name' => $txt['no_primary_membergroup'],
2847
			'is_primary' => $cur_profile['id_group'] == 0,
2848
			'can_be_additional' => false,
2849
			'can_be_primary' => true,
2850
		)
2851
	);
2852
	$curGroups = explode(',', $cur_profile['additional_groups']);
2853
2854
	// Load membergroups, but only those groups the user can assign.
2855
	$request = $smcFunc['db_query']('', '
2856
		SELECT group_name, id_group, hidden
2857
		FROM {db_prefix}membergroups
2858
		WHERE id_group != {int:moderator_group}
2859
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
2860
			AND group_type != {int:is_protected}') . '
2861
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
2862
		array(
2863
			'moderator_group' => 3,
2864
			'min_posts' => -1,
2865
			'is_protected' => 1,
2866
			'newbie_group' => 4,
2867
		)
2868
	);
2869
	while ($row = $smcFunc['db_fetch_assoc']($request))
2870
	{
2871
		// We should skip the administrator group if they don't have the admin_forum permission!
2872
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
2873
			continue;
2874
2875
		$context['member_groups'][$row['id_group']] = array(
2876
			'id' => $row['id_group'],
2877
			'name' => $row['group_name'],
2878
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
2879
			'is_additional' => in_array($row['id_group'], $curGroups),
2880
			'can_be_additional' => true,
2881
			'can_be_primary' => $row['hidden'] != 2,
2882
		);
2883
	}
2884
	$smcFunc['db_free_result']($request);
2885
2886
	$context['member']['group_id'] = $user_settings['id_group'];
2887
2888
	return true;
2889
}
2890
2891
/**
2892
 * Load key signature context data.
2893
 *
2894
 * @return true Always returns true
2895
 */
2896
function profileLoadSignatureData()
2897
{
2898
	global $modSettings, $context, $txt, $cur_profile, $memberContext;
2899
2900
	// Signature limits.
2901
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
2902
	$sig_limits = explode(',', $sig_limits);
2903
2904
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
2905
	$context['signature_limits'] = array(
2906
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
2907
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
2908
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
2909
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
2910
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
2911
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
2912
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
2913
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
2914
	);
2915
	// Kept this line in for backwards compatibility!
2916
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
2917
	// Warning message for signature image limits?
2918
	$context['signature_warning'] = '';
2919
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
2920
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
2921
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
2922
		$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']);
2923
2924
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_charset'] == 'UTF-8' || function_exists('iconv'))));
2925
2926
	if (empty($context['do_preview']))
2927
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
2928
	else
2929
	{
2930
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
2931
		$validation = profileValidateSignature($signature);
2932
		if (empty($context['post_errors']))
2933
		{
2934
			loadLanguage('Errors');
2935
			$context['post_errors'] = array();
2936
		}
2937
		$context['post_errors'][] = 'signature_not_yet_saved';
2938
		if ($validation !== true && $validation !== false)
2939
			$context['post_errors'][] = $validation;
2940
2941
		censorText($context['member']['signature']);
2942
		$context['member']['current_signature'] = $context['member']['signature'];
2943
		censorText($signature);
2944
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']]);
2945
		$context['member']['signature'] = $_POST['signature'];
2946
	}
2947
2948
	// Load the spell checker?
2949
	if ($context['show_spellchecking'])
2950
		loadJavaScriptFile('spellcheck.js', array('defer' => false), 'smf_spellcheck');
2951
2952
	return true;
2953
}
2954
2955
/**
2956
 * Load avatar context data.
2957
 *
2958
 * @return true Always returns true
2959
 */
2960
function profileLoadAvatarData()
2961
{
2962
	global $context, $cur_profile, $modSettings, $scripturl;
2963
2964
	$context['avatar_url'] = $modSettings['avatar_url'];
2965
2966
	// Default context.
2967
	$context['member']['avatar'] += array(
2968
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
2969
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
2970
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2971
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2972
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2973
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']) || !empty($modSettings['gravatarOverride']),
2974
	);
2975
2976
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
2977
	{
2978
		$context['member']['avatar'] += array(
2979
			'choice' => 'gravatar',
2980
			'server_pic' => 'blank.png',
2981
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || !empty($modSettings['gravatarOverride']) ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
2982
		);
2983
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
2984
	}
2985
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
2986
	{
2987
		$context['member']['avatar'] += array(
2988
			'choice' => 'upload',
2989
			'server_pic' => 'blank.png',
2990
			'external' => 'http://'
2991
		);
2992
		$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'];
2993
	}
2994
	// Use "avatar_original" here so we show what the user entered even if the image proxy is enabled
2995
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
2996
		$context['member']['avatar'] += array(
2997
			'choice' => 'external',
2998
			'server_pic' => 'blank.png',
2999
			'external' => $cur_profile['avatar_original']
3000
		);
3001
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
3002
		$context['member']['avatar'] += array(
3003
			'choice' => 'server_stored',
3004
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3005
			'external' => 'http://'
3006
		);
3007
	else
3008
		$context['member']['avatar'] += array(
3009
			'choice' => 'none',
3010
			'server_pic' => 'blank.png',
3011
			'external' => 'http://'
3012
		);
3013
3014
	// Get a list of all the avatars.
3015
	if ($context['member']['avatar']['allow_server_stored'])
3016
	{
3017
		$context['avatar_list'] = array();
3018
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3019
	}
3020
	else
3021
		$context['avatars'] = array();
3022
3023
	// Second level selected avatar...
3024
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3025
	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']);
3026
}
3027
3028
/**
3029
 * Save a members group.
3030
 *
3031
 * @param int &$value The ID of the (new) primary group
3032
 * @return true Always returns true
3033
 */
3034
function profileSaveGroups(&$value)
3035
{
3036
	global $profile_vars, $old_profile, $context, $smcFunc, $cur_profile;
3037
3038
	// Do we need to protect some groups?
3039 View Code Duplication
	if (!allowedTo('admin_forum'))
3040
	{
3041
		$request = $smcFunc['db_query']('', '
3042
			SELECT id_group
3043
			FROM {db_prefix}membergroups
3044
			WHERE group_type = {int:is_protected}',
3045
			array(
3046
				'is_protected' => 1,
3047
			)
3048
		);
3049
		$protected_groups = array(1);
3050
		while ($row = $smcFunc['db_fetch_assoc']($request))
3051
			$protected_groups[] = $row['id_group'];
3052
		$smcFunc['db_free_result']($request);
3053
3054
		$protected_groups = array_unique($protected_groups);
3055
	}
3056
3057
	// The account page allows the change of your id_group - but not to a protected group!
3058
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3059
		$value = (int) $value;
3060
	// ... otherwise it's the old group sir.
3061
	else
3062
		$value = $old_profile['id_group'];
3063
3064
	// Find the additional membergroups (if any)
3065
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3066
	{
3067
		$additional_groups = array();
3068
		foreach ($_POST['additional_groups'] as $group_id)
3069
		{
3070
			$group_id = (int) $group_id;
3071
			if (!empty($group_id) && (empty($protected_groups) || !in_array($group_id, $protected_groups)))
0 ignored issues
show
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...
3072
				$additional_groups[] = $group_id;
3073
		}
3074
3075
		// Put the protected groups back in there if you don't have permission to take them away.
3076
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3077
		foreach ($old_additional_groups as $group_id)
3078
		{
3079
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3080
				$additional_groups[] = $group_id;
3081
		}
3082
3083
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3084
		{
3085
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3086
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3087
		}
3088
	}
3089
3090
	// Too often, people remove delete their own account, or something.
3091
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3092
	{
3093
		$stillAdmin = $value == 1 || (isset($additional_groups) && in_array(1, $additional_groups));
0 ignored issues
show
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...
3094
3095
		// If they would no longer be an admin, look for any other...
3096
		if (!$stillAdmin)
3097
		{
3098
			$request = $smcFunc['db_query']('', '
3099
				SELECT id_member
3100
				FROM {db_prefix}members
3101
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3102
					AND id_member != {int:selected_member}
3103
				LIMIT 1',
3104
				array(
3105
					'admin_group' => 1,
3106
					'selected_member' => $context['id_member'],
3107
				)
3108
			);
3109
			list ($another) = $smcFunc['db_fetch_row']($request);
3110
			$smcFunc['db_free_result']($request);
3111
3112
			if (empty($another))
3113
				fatal_lang_error('at_least_one_admin', 'critical');
3114
		}
3115
	}
3116
3117
	// If we are changing group status, update permission cache as necessary.
3118
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3119
	{
3120 View Code Duplication
		if ($context['user']['is_owner'])
3121
			$_SESSION['mc']['time'] = 0;
3122
		else
3123
			updateSettings(array('settings_updated' => time()));
3124
	}
3125
3126
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3127
	call_integration_hook('integrate_profile_profileSaveGroups', array($value, $additional_groups));
3128
3129
	return true;
3130
}
3131
3132
/**
3133
 * The avatar is incredibly complicated, what with the options... and what not.
3134
 * @todo argh, the avatar here. Take this out of here!
3135
 *
3136
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3137
 * @return bool|string False if success (or if memID is empty and password authentication failed), otherwise a string indicating what error occurred
3138
 */
3139
function profileSaveAvatarData(&$value)
3140
{
3141
	global $modSettings, $sourcedir, $smcFunc, $profile_vars, $cur_profile, $context;
3142
3143
	$memID = $context['id_member'];
3144
	if (empty($memID) && !empty($context['password_auth_failed']))
3145
		return false;
3146
3147
	require_once($sourcedir . '/ManageAttachments.php');
3148
3149
	// We're going to put this on a nice custom dir.
3150
	$uploadDir = $modSettings['custom_avatar_dir'];
3151
	$id_folder = 1;
3152
3153
	$downloadedExternalAvatar = false;
3154
	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']))
3155
	{
3156
		if (!is_writable($uploadDir))
3157
			fatal_lang_error('attachments_no_write', 'critical');
3158
3159
		require_once($sourcedir . '/Subs-Package.php');
3160
3161
		$url = parse_url($_POST['userpicpersonal']);
3162
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3163
3164
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
3165
		if ($contents != false && $tmpAvatar = fopen($new_filename, 'wb'))
0 ignored issues
show
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...
3166
		{
3167
			fwrite($tmpAvatar, $contents);
3168
			fclose($tmpAvatar);
3169
3170
			$downloadedExternalAvatar = true;
3171
			$_FILES['attachment']['tmp_name'] = $new_filename;
3172
		}
3173
	}
3174
3175
	// Removes whatever attachment there was before updating
3176
	if ($value == 'none')
3177
	{
3178
		$profile_vars['avatar'] = '';
3179
3180
		// Reset the attach ID.
3181
		$cur_profile['id_attach'] = 0;
3182
		$cur_profile['attachment_type'] = 0;
3183
		$cur_profile['filename'] = '';
3184
3185
		removeAttachments(array('id_member' => $memID));
3186
	}
3187
3188
	// An avatar from the server-stored galleries.
3189
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3190
	{
3191
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3192
		$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']) : '';
3193
3194
		// Clear current profile...
3195
		$cur_profile['id_attach'] = 0;
3196
		$cur_profile['attachment_type'] = 0;
3197
		$cur_profile['filename'] = '';
3198
3199
		// Get rid of their old avatar. (if uploaded.)
3200
		removeAttachments(array('id_member' => $memID));
3201
	}
3202
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3203
	{
3204
		// 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.
3205
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3206
			$profile_vars['avatar'] = 'gravatar://';
3207
		else
3208
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3209
3210
		// Get rid of their old avatar. (if uploaded.)
3211
		removeAttachments(array('id_member' => $memID));
3212
	}
3213
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3214
	{
3215
		// We need these clean...
3216
		$cur_profile['id_attach'] = 0;
3217
		$cur_profile['attachment_type'] = 0;
3218
		$cur_profile['filename'] = '';
3219
3220
		// Remove any attached avatar...
3221
		removeAttachments(array('id_member' => $memID));
3222
3223
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3224
3225
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3226
			$profile_vars['avatar'] = '';
3227
		// Trying to make us do something we'll regret?
3228
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3229
			return 'bad_avatar_invalid_url';
3230
		// Should we check dimensions?
3231
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3232
		{
3233
			// Now let's validate the avatar.
3234
			$sizes = url_image_size($profile_vars['avatar']);
3235
3236
			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']))))
3237
			{
3238
				// Houston, we have a problem. The avatar is too large!!
3239
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3240
					return 'bad_avatar_too_large';
3241
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3242
				{
3243
					// @todo remove this if appropriate
3244
					require_once($sourcedir . '/Subs-Graphics.php');
3245
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3246
					{
3247
						$profile_vars['avatar'] = '';
3248
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3249
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3250
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3251
					}
3252
					else
3253
						return 'bad_avatar';
3254
				}
3255
			}
3256
		}
3257
	}
3258
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3259
	{
3260
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3261
		{
3262
			// Get the dimensions of the image.
3263
			if (!$downloadedExternalAvatar)
3264
			{
3265
				if (!is_writable($uploadDir))
3266
					fatal_lang_error('attachments_no_write', 'critical');
3267
3268
				$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
3269
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3270
					fatal_lang_error('attach_timeout', 'critical');
3271
3272
				$_FILES['attachment']['tmp_name'] = $new_filename;
3273
			}
3274
3275
			$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3276
3277
			// No size, then it's probably not a valid pic.
3278
			if ($sizes === false)
3279
			{
3280
				@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...
3281
				return 'bad_avatar';
3282
			}
3283
			// Check whether the image is too large.
3284
			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']))
3285
			{
3286
				if (!empty($modSettings['avatar_resize_upload']))
3287
				{
3288
					// Attempt to chmod it.
3289
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3290
3291
					// @todo remove this require when appropriate
3292
					require_once($sourcedir . '/Subs-Graphics.php');
3293
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3294
					{
3295
						@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...
3296
						return 'bad_avatar';
3297
					}
3298
3299
					// Reset attachment avatar data.
3300
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3301
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3302
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3303
				}
3304
3305
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3306
				else
3307
				{
3308
					@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...
3309
					return 'bad_avatar_too_large';
3310
				}
3311
			}
3312
3313
			// So far, so good, checks lies ahead!
3314
			elseif (is_array($sizes))
3315
			{
3316
				// Now try to find an infection.
3317
				require_once($sourcedir . '/Subs-Graphics.php');
3318
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3319
				{
3320
					// It's bad. Try to re-encode the contents?
3321
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3322
					{
3323
						@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...
3324
						return 'bad_avatar_fail_reencode';
3325
					}
3326
					// We were successful. However, at what price?
3327
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3328
					// Hard to believe this would happen, but can you bet?
3329
					if ($sizes === false)
3330
					{
3331
						@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...
3332
						return 'bad_avatar';
3333
					}
3334
				}
3335
3336
				$extensions = array(
3337
					'1' => 'gif',
3338
					'2' => 'jpg',
3339
					'3' => 'png',
3340
					'6' => 'bmp'
3341
				);
3342
3343
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3344
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3345
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3346
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3347
				$file_hash = '';
3348
3349
				// Remove previous attachments this member might have had.
3350
				removeAttachments(array('id_member' => $memID));
3351
3352
				$cur_profile['id_attach'] = $smcFunc['db_insert']('',
3353
					'{db_prefix}attachments',
3354
					array(
3355
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3356
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3357
					),
3358
					array(
3359
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3360
						(int) $width, (int) $height, $mime_type, $id_folder,
3361
					),
3362
					array('id_attach'),
3363
					1
3364
				);
3365
3366
				$cur_profile['filename'] = $destName;
3367
				$cur_profile['attachment_type'] = 1;
3368
3369
				$destinationPath = $uploadDir . '/' . (empty($file_hash) ? $destName : $cur_profile['id_attach'] . '_' . $file_hash . '.dat');
3370
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3371
				{
3372
					// I guess a man can try.
3373
					removeAttachments(array('id_member' => $memID));
3374
					fatal_lang_error('attach_timeout', 'critical');
3375
				}
3376
3377
				// Attempt to chmod it.
3378
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3379
			}
3380
			$profile_vars['avatar'] = '';
3381
3382
			// Delete any temporary file.
3383
			if (file_exists($_FILES['attachment']['tmp_name']))
3384
				@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...
3385
		}
3386
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3387
		else
3388
			$profile_vars['avatar'] = '';
3389
	}
3390
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3391
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3392
	else
3393
		$profile_vars['avatar'] = '';
3394
3395
	// Setup the profile variables so it shows things right on display!
3396
	$cur_profile['avatar'] = $profile_vars['avatar'];
3397
3398
	return false;
3399
}
3400
3401
/**
3402
 * Validate the signature
3403
 *
3404
 * @param string &$value The new signature
3405
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3406
 */
3407
function profileValidateSignature(&$value)
3408
{
3409
	global $sourcedir, $modSettings, $smcFunc, $txt;
3410
3411
	require_once($sourcedir . '/Subs-Post.php');
3412
3413
	// Admins can do whatever they hell they want!
3414
	if (!allowedTo('admin_forum'))
3415
	{
3416
		// Load all the signature limits.
3417
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3418
		$sig_limits = explode(',', $sig_limits);
3419
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3420
3421
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3422
3423
		// Too many lines?
3424
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3425
		{
3426
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3427
			return 'signature_max_lines';
3428
		}
3429
3430
		// Too many images?!
3431 View Code Duplication
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
3432
		{
3433
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3434
			return 'signature_max_image_count';
3435
		}
3436
3437
		// What about too many smileys!
3438
		$smiley_parsed = $unparsed_signature;
3439
		parsesmileys($smiley_parsed);
3440
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3441
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3442
			return 'signature_allow_smileys';
3443
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3444
		{
3445
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3446
			return 'signature_max_smileys';
3447
		}
3448
3449
		// Maybe we are abusing font sizes?
3450
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3451
		{
3452
			foreach ($matches[1] as $ind => $size)
3453
			{
3454
				$limit_broke = 0;
3455
				// Attempt to allow all sizes of abuse, so to speak.
3456 View Code Duplication
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
3457
					$limit_broke = $sig_limits[7] . 'px';
3458
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3459
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3460
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3461
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3462
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3463
					$limit_broke = 'large';
3464
3465
				if ($limit_broke)
3466
				{
3467
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3468
					return 'signature_max_font_size';
3469
				}
3470
			}
3471
		}
3472
3473
		// The difficult one - image sizes! Don't error on this - just fix it.
3474
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3475
		{
3476
			// Get all BBC tags...
3477
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3478
			// ... and all HTML ones.
3479
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?' . '>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3480
			// And stick the HTML in the BBC.
3481 View Code Duplication
			if (!empty($matches2))
3482
			{
3483
				foreach ($matches2[0] as $ind => $dummy)
3484
				{
3485
					$matches[0][] = $matches2[0][$ind];
3486
					$matches[1][] = '';
3487
					$matches[2][] = '';
3488
					$matches[3][] = '';
3489
					$matches[4][] = '';
3490
					$matches[5][] = '';
3491
					$matches[6][] = '';
3492
					$matches[7][] = $matches2[1][$ind];
3493
				}
3494
			}
3495
3496
			$replaces = array();
3497
			// Try to find all the images!
3498
			if (!empty($matches))
3499
			{
3500
				foreach ($matches[0] as $key => $image)
3501
				{
3502
					$width = -1; $height = -1;
3503
3504
					// Does it have predefined restraints? Width first.
3505 View Code Duplication
					if ($matches[6][$key])
3506
						$matches[2][$key] = $matches[6][$key];
3507 View Code Duplication
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
3508
					{
3509
						$width = $sig_limits[5];
3510
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3511
					}
3512
					elseif ($matches[2][$key])
3513
						$width = $matches[2][$key];
3514
					// ... and height.
3515 View Code Duplication
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
3516
					{
3517
						$height = $sig_limits[6];
3518
						if ($width != -1)
3519
							$width = $width * ($height / $matches[4][$key]);
3520
					}
3521
					elseif ($matches[4][$key])
3522
						$height = $matches[4][$key];
3523
3524
					// If the dimensions are still not fixed - we need to check the actual image.
3525 View Code Duplication
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
3526
					{
3527
						$sizes = url_image_size($matches[7][$key]);
3528
						if (is_array($sizes))
3529
						{
3530
							// Too wide?
3531
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3532
							{
3533
								$width = $sig_limits[5];
3534
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3535
							}
3536
							// Too high?
3537
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3538
							{
3539
								$height = $sig_limits[6];
3540
								if ($width == -1)
3541
									$width = $sizes[0];
3542
								$width = $width * ($height / $sizes[1]);
3543
							}
3544
							elseif ($width != -1)
3545
								$height = $sizes[1];
3546
						}
3547
					}
3548
3549
					// Did we come up with some changes? If so remake the string.
3550 View Code Duplication
					if ($width != -1 || $height != -1)
3551
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3552
				}
3553
				if (!empty($replaces))
3554
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3555
			}
3556
		}
3557
3558
		// Any disabled BBC?
3559
		$disabledSigBBC = implode('|', $disabledTags);
3560
		if (!empty($disabledSigBBC))
3561
		{
3562
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3563
			{
3564
				$disabledTags = array_unique($disabledTags);
3565
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3566
				return 'signature_disabled_bbc';
3567
			}
3568
		}
3569
	}
3570
3571
	preparsecode($value);
3572
3573
	// Too long?
3574
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3575
	{
3576
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3577
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3578
		return 'signature_max_length';
3579
	}
3580
3581
	return true;
3582
}
3583
3584
/**
3585
 * Validate an email address.
3586
 *
3587
 * @param string $email The email address to validate
3588
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3589
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3590
 */
3591
function profileValidateEmail($email, $memID = 0)
3592
{
3593
	global $smcFunc;
3594
3595
	$email = strtr($email, array('&#039;' => '\''));
3596
3597
	// Check the name and email for validity.
3598
	if (trim($email) == '')
3599
		return 'no_email';
3600
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3601
		return 'bad_email';
3602
3603
	// Email addresses should be and stay unique.
3604
	$request = $smcFunc['db_query']('', '
3605
		SELECT id_member
3606
		FROM {db_prefix}members
3607
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3608
			email_address = {string:email_address}
3609
		LIMIT 1',
3610
		array(
3611
			'selected_member' => $memID,
3612
			'email_address' => $email,
3613
		)
3614
	);
3615
3616
	if ($smcFunc['db_num_rows']($request) > 0)
3617
		return 'email_taken';
3618
	$smcFunc['db_free_result']($request);
3619
3620
	return true;
3621
}
3622
3623
/**
3624
 * Reload a user's settings.
3625
 */
3626
function profileReloadUser()
3627
{
3628
	global $modSettings, $context, $cur_profile;
3629
3630
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3631
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3632
3633
	loadUserSettings();
3634
	writeLog();
3635
}
3636
3637
/**
3638
 * Send the user a new activation email if they need to reactivate!
3639
 */
3640
function profileSendActivation()
3641
{
3642
	global $sourcedir, $profile_vars, $context, $scripturl, $smcFunc, $cookiename, $cur_profile, $language, $modSettings;
3643
3644
	require_once($sourcedir . '/Subs-Post.php');
3645
3646
	// Shouldn't happen but just in case.
3647
	if (empty($profile_vars['email_address']))
3648
		return;
3649
3650
	$replacements = array(
3651
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3652
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3653
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3654
	);
3655
3656
	// Send off the email.
3657
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3658
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3659
3660
	// Log the user out.
3661
	$smcFunc['db_query']('', '
3662
		DELETE FROM {db_prefix}log_online
3663
		WHERE id_member = {int:selected_member}',
3664
		array(
3665
			'selected_member' => $context['id_member'],
3666
		)
3667
	);
3668
	$_SESSION['log_time'] = 0;
3669
	$_SESSION['login_' . $cookiename] = $smcFunc['json_encode'](array(0, '', 0));
3670
3671
	if (isset($_COOKIE[$cookiename]))
3672
		$_COOKIE[$cookiename] = '';
3673
3674
	loadUserSettings();
3675
3676
	$context['user']['is_logged'] = false;
3677
	$context['user']['is_guest'] = true;
3678
3679
	redirectexit('action=sendactivation');
3680
}
3681
3682
/**
3683
 * Function to allow the user to choose group membership etc...
3684
 *
3685
 * @param int $memID The ID of the member
3686
 */
3687
function groupMembership($memID)
3688
{
3689
	global $txt, $user_profile, $context, $smcFunc;
3690
3691
	$curMember = $user_profile[$memID];
3692
	$context['primary_group'] = $curMember['id_group'];
3693
3694
	// Can they manage groups?
3695
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3696
	$context['can_manage_protected'] = allowedTo('admin_forum');
3697
	$context['can_edit_primary'] = $context['can_manage_protected'];
3698
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
3699
3700
	// Get all the groups this user is a member of.
3701
	$groups = explode(',', $curMember['additional_groups']);
3702
	$groups[] = $curMember['id_group'];
3703
3704
	// Ensure the query doesn't croak!
3705
	if (empty($groups))
3706
		$groups = array(0);
3707
	// Just to be sure...
3708
	foreach ($groups as $k => $v)
3709
		$groups[$k] = (int) $v;
3710
3711
	// Get all the membergroups they can join.
3712
	$request = $smcFunc['db_query']('', '
3713
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
3714
			COALESCE(lgr.id_member, 0) AS pending
3715
		FROM {db_prefix}membergroups AS mg
3716
			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})
3717
		WHERE (mg.id_group IN ({array_int:group_list})
3718
			OR mg.group_type > {int:nonjoin_group_id})
3719
			AND mg.min_posts = {int:min_posts}
3720
			AND mg.id_group != {int:moderator_group}
3721
		ORDER BY group_name',
3722
		array(
3723
			'group_list' => $groups,
3724
			'selected_member' => $memID,
3725
			'status_open' => 0,
3726
			'nonjoin_group_id' => 1,
3727
			'min_posts' => -1,
3728
			'moderator_group' => 3,
3729
		)
3730
	);
3731
	// This beast will be our group holder.
3732
	$context['groups'] = array(
3733
		'member' => array(),
3734
		'available' => array()
3735
	);
3736
	while ($row = $smcFunc['db_fetch_assoc']($request))
3737
	{
3738
		// Can they edit their primary group?
3739
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
3740
			$context['can_edit_primary'] = true;
3741
3742
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
3743
		if (((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0)) && $row['id_group'] != $context['primary_group'])
3744
			continue;
3745
3746
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
3747
			'id' => $row['id_group'],
3748
			'name' => $row['group_name'],
3749
			'desc' => $row['description'],
3750
			'color' => $row['online_color'],
3751
			'type' => $row['group_type'],
3752
			'pending' => $row['pending'],
3753
			'is_primary' => $row['id_group'] == $context['primary_group'],
3754
			'can_be_primary' => $row['hidden'] != 2,
3755
			// Anything more than this needs to be done through account settings for security.
3756
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
3757
		);
3758
	}
3759
	$smcFunc['db_free_result']($request);
3760
3761
	// Add registered members on the end.
3762
	$context['groups']['member'][0] = array(
3763
		'id' => 0,
3764
		'name' => $txt['regular_members'],
3765
		'desc' => $txt['regular_members_desc'],
3766
		'type' => 0,
3767
		'is_primary' => $context['primary_group'] == 0 ? true : false,
3768
		'can_be_primary' => true,
3769
		'can_leave' => 0,
3770
	);
3771
3772
	// No changing primary one unless you have enough groups!
3773
	if (count($context['groups']['member']) < 2)
3774
		$context['can_edit_primary'] = false;
3775
3776
	// In the special case that someone is requesting membership of a group, setup some special context vars.
3777
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
3778
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
3779
}
3780
3781
/**
3782
 * This function actually makes all the group changes
3783
 *
3784
 * @param array $profile_vars The profile variables
3785
 * @param array $post_errors Any errors that have occurred
3786
 * @param int $memID The ID of the member
3787
 * @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
3788
 */
3789
function groupMembership2($profile_vars, $post_errors, $memID)
3790
{
3791
	global $user_info, $context, $user_profile, $modSettings, $smcFunc;
3792
3793
	// Let's be extra cautious...
3794
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
3795
		isAllowedTo('manage_membergroups');
3796
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
3797
		fatal_lang_error('no_access', false);
3798
3799
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
3800
3801
	$old_profile = &$user_profile[$memID];
3802
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3803
	$context['can_manage_protected'] = allowedTo('admin_forum');
3804
3805
	// By default the new primary is the old one.
3806
	$newPrimary = $old_profile['id_group'];
3807
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
3808
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
3809
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
3810
3811
	// One way or another, we have a target group in mind...
3812
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
3813
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
3814
3815
	// Sanity check!!
3816
	if ($group_id == 1)
3817
		isAllowedTo('admin_forum');
3818
	// Protected groups too!
3819
	else
3820
	{
3821
		$request = $smcFunc['db_query']('', '
3822
			SELECT group_type
3823
			FROM {db_prefix}membergroups
3824
			WHERE id_group = {int:current_group}
3825
			LIMIT {int:limit}',
3826
			array(
3827
				'current_group' => $group_id,
3828
				'limit' => 1,
3829
			)
3830
		);
3831
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
3832
		$smcFunc['db_free_result']($request);
3833
3834
		if ($is_protected == 1)
3835
			isAllowedTo('admin_forum');
3836
	}
3837
3838
	// What ever we are doing, we need to determine if changing primary is possible!
3839
	$request = $smcFunc['db_query']('', '
3840
		SELECT id_group, group_type, hidden, group_name
3841
		FROM {db_prefix}membergroups
3842
		WHERE id_group IN ({int:group_list}, {int:current_group})',
3843
		array(
3844
			'group_list' => $group_id,
3845
			'current_group' => $old_profile['id_group'],
3846
		)
3847
	);
3848
	while ($row = $smcFunc['db_fetch_assoc']($request))
3849
	{
3850
		// Is this the new group?
3851
		if ($row['id_group'] == $group_id)
3852
		{
3853
			$foundTarget = true;
3854
			$group_name = $row['group_name'];
3855
3856
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
3857
			if ($changeType == 'request' && $row['group_type'] != 2)
3858
				fatal_lang_error('no_access', false);
3859
			// What about leaving a requestable group we are not a member of?
3860
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
3861
				fatal_lang_error('no_access', false);
3862
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
3863
				fatal_lang_error('no_access', false);
3864
3865
			// We can't change the primary group if this is hidden!
3866
			if ($row['hidden'] == 2)
3867
				$canChangePrimary = false;
3868
		}
3869
3870
		// If this is their old primary, can we change it?
3871 View Code Duplication
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
3872
			$canChangePrimary = 1;
3873
3874
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
3875
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
3876
			$canChangePrimary = false;
3877
3878
		// If this is the one we are acting on, can we even act?
3879 View Code Duplication
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
3880
			$canChangePrimary = false;
3881
	}
3882
	$smcFunc['db_free_result']($request);
3883
3884
	// Didn't find the target?
3885
	if (!$foundTarget)
3886
		fatal_lang_error('no_access', false);
3887
3888
	// Final security check, don't allow users to promote themselves to admin.
3889
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
3890
	{
3891
		$request = $smcFunc['db_query']('', '
3892
			SELECT COUNT(permission)
3893
			FROM {db_prefix}permissions
3894
			WHERE id_group = {int:selected_group}
3895
				AND permission = {string:admin_forum}
3896
				AND add_deny = {int:not_denied}',
3897
			array(
3898
				'selected_group' => $group_id,
3899
				'not_denied' => 1,
3900
				'admin_forum' => 'admin_forum',
3901
			)
3902
		);
3903
		list ($disallow) = $smcFunc['db_fetch_row']($request);
3904
		$smcFunc['db_free_result']($request);
3905
3906
		if ($disallow)
3907
			isAllowedTo('admin_forum');
3908
	}
3909
3910
	// If we're requesting, add the note then return.
3911
	if ($changeType == 'request')
3912
	{
3913
		$request = $smcFunc['db_query']('', '
3914
			SELECT id_member
3915
			FROM {db_prefix}log_group_requests
3916
			WHERE id_member = {int:selected_member}
3917
				AND id_group = {int:selected_group}
3918
				AND status = {int:status_open}',
3919
			array(
3920
				'selected_member' => $memID,
3921
				'selected_group' => $group_id,
3922
				'status_open' => 0,
3923
			)
3924
		);
3925
		if ($smcFunc['db_num_rows']($request) != 0)
3926
			fatal_lang_error('profile_error_already_requested_group');
3927
		$smcFunc['db_free_result']($request);
3928
3929
		// Log the request.
3930
		$smcFunc['db_insert']('',
3931
			'{db_prefix}log_group_requests',
3932
			array(
3933
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
3934
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
3935
			),
3936
			array(
3937
				$memID, $group_id, time(), $_POST['reason'],
3938
				0, 0, '', 0, '',
3939
			),
3940
			array('id_request')
3941
		);
3942
3943
		// Set up some data for our background task...
3944
		$data = $smcFunc['json_encode'](array('id_member' => $memID, 'member_name' => $user_info['name'], 'id_group' => $group_id, 'group_name' => $group_name, 'reason' => $_POST['reason'], 'time' => time()));
0 ignored issues
show
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...
3945
3946
		// Add a background task to handle notifying people of this request
3947
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
3948
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
3949
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
3950
		);
3951
3952
		return $changeType;
3953
	}
3954
	// Otherwise we are leaving/joining a group.
3955
	elseif ($changeType == 'free')
3956
	{
3957
		// Are we leaving?
3958
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
3959
		{
3960
			if ($old_profile['id_group'] == $group_id)
3961
				$newPrimary = 0;
3962
			else
3963
				unset($addGroups[$group_id]);
3964
		}
3965
		// ... if not, must be joining.
3966
		else
3967
		{
3968
			// Can we change the primary, and do we want to?
3969
			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...
3970
			{
3971
				if ($old_profile['id_group'] != 0)
3972
					$addGroups[$old_profile['id_group']] = -1;
3973
				$newPrimary = $group_id;
3974
			}
3975
			// Otherwise it's an additional group...
3976
			else
3977
				$addGroups[$group_id] = -1;
3978
		}
3979
	}
3980
	// Finally, we must be setting the primary.
3981
	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...
3982
	{
3983
		if ($old_profile['id_group'] != 0)
3984
			$addGroups[$old_profile['id_group']] = -1;
3985
		if (isset($addGroups[$group_id]))
3986
			unset($addGroups[$group_id]);
3987
		$newPrimary = $group_id;
3988
	}
3989
3990
	// Finally, we can make the changes!
3991
	foreach ($addGroups as $id => $dummy)
3992
		if (empty($id))
3993
			unset($addGroups[$id]);
3994
	$addGroups = implode(',', array_flip($addGroups));
3995
3996
	// Ensure that we don't cache permissions if the group is changing.
3997 View Code Duplication
	if ($context['user']['is_owner'])
3998
		$_SESSION['mc']['time'] = 0;
3999
	else
4000
		updateSettings(array('settings_updated' => time()));
4001
4002
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
4003
4004
	return $changeType;
4005
}
4006
4007
/**
4008
 * Provides interface to setup Two Factor Auth in SMF
4009
 *
4010
 * @param int $memID The ID of the member
4011
 */
4012
function tfasetup($memID)
4013
{
4014
	global $user_info, $context, $user_settings, $sourcedir, $modSettings;
4015
4016
	require_once($sourcedir . '/Class-TOTP.php');
4017
	require_once($sourcedir . '/Subs-Auth.php');
4018
4019
	// If TFA has not been setup, allow them to set it up
4020
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4021
	{
4022
		// Check to ensure we're forcing SSL for authentication
4023 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
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...
4024
			fatal_lang_error('login_ssl_required');
4025
4026
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4027
		// we do not want too much AJAX to confuse them.
4028
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4029
		{
4030
			$context['from_ajax'] = true;
4031
			$context['template_layers'] = array();
4032
		}
4033
4034
		// When the code is being sent, verify to make sure the user got it right
4035
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4036
		{
4037
			$code = $_POST['tfa_code'];
4038
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4039
			$totp->setRange(1);
4040
			$valid_password = hash_verify_password($user_settings['member_name'], trim($_POST['passwd']), $user_settings['passwd']);
4041
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4042
4043
			if ($valid_password && $valid_code)
4044
			{
4045
				$backup = substr(sha1(mt_rand()), 0, 16);
4046
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4047
4048
				updateMemberData($memID, array(
4049
					'tfa_secret' => $_SESSION['tfa_secret'],
4050
					'tfa_backup' => $backup_encrypted,
4051
				));
4052
4053
				setTFACookie(3153600, $memID, hash_salt($backup_encrypted, $user_settings['password_salt']));
4054
4055
				unset($_SESSION['tfa_secret']);
4056
4057
				$context['tfa_backup'] = $backup;
4058
				$context['sub_template'] = 'tfasetup_backup';
4059
4060
				return;
4061
			}
4062
			else
4063
			{
4064
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4065
				$context['tfa_error'] = !$valid_code;
4066
				$context['tfa_pass_error'] = !$valid_password;
4067
				$context['tfa_pass_value'] = $_POST['passwd'];
4068
				$context['tfa_value'] = $_POST['tfa_code'];
4069
			}
4070
		}
4071
		else
4072
		{
4073
			$totp = new \TOTP\Auth();
4074
			$secret = $totp->generateCode();
4075
			$_SESSION['tfa_secret'] = $secret;
4076
			$context['tfa_secret'] = $secret;
4077
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4078
		}
4079
4080
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4081
	}
4082
	elseif (isset($_REQUEST['disable']))
4083
	{
4084
		updateMemberData($memID, array(
4085
			'tfa_secret' => '',
4086
			'tfa_backup' => '',
4087
		));
4088
		redirectexit('action=profile;area=account;u=' . $memID);
4089
	}
4090
	else
4091
		redirectexit('action=profile;area=account;u=' . $memID);
4092
}
4093
4094
?>