Passed
Push — release-2.1 ( 450da8...d54fa3 )
by Mathias
45s
created

alert_count()   C

Complexity

Conditions 14
Paths 61

Size

Total Lines 64
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 32
c 0
b 0
f 0
nc 61
nop 2
dl 0
loc 64
rs 6.2666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has the primary job of showing and editing people's profiles.
5
 * 	It also allows the user to change some of their or another's preferences,
6
 * 	and such things
7
 *
8
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines http://www.simplemachines.org
12
 * @copyright 2018 Simple Machines and individual contributors
13
 * @license http://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1 Beta 4
16
 */
17
18
if (!defined('SMF'))
19
	die('No direct access...');
20
21
/**
22
 * This defines every profile field known to man.
23
 *
24
 * @param bool $force_reload Whether to reload the data
25
 */
26
function loadProfileFields($force_reload = false)
27
{
28
	global $context, $profile_fields, $txt, $scripturl, $modSettings, $user_info, $smcFunc, $cur_profile, $language;
29
	global $sourcedir, $profile_vars;
30
31
	// Don't load this twice!
32
	if (!empty($profile_fields) && !$force_reload)
33
		return;
34
35
	/* This horrific array defines all the profile fields in the whole world!
36
		In general each "field" has one array - the key of which is the database column name associated with said field. Each item
37
		can have the following attributes:
38
39
				string $type:			The type of field this is - valid types are:
40
					- callback:		This is a field which has its own callback mechanism for templating.
41
					- check:		A simple checkbox.
42
					- hidden:		This doesn't have any visual aspects but may have some validity.
43
					- password:		A password box.
44
					- select:		A select box.
45
					- text:			A string of some description.
46
47
				string $label:			The label for this item - default will be $txt[$key] if this isn't set.
48
				string $subtext:		The subtext (Small label) for this item.
49
				int $size:			Optional size for a text area.
50
				array $input_attr:		An array of text strings to be added to the input box for this item.
51
				string $value:			The value of the item. If not set $cur_profile[$key] is assumed.
52
				string $permission:		Permission required for this item (Excluded _any/_own subfix which is applied automatically).
53
				function $input_validate:	A runtime function which validates the element before going to the database. It is passed
54
								the relevant $_POST element if it exists and should be treated like a reference.
55
56
								Return types:
57
					- true:			Element can be stored.
58
					- false:		Skip this element.
59
					- a text string:	An error occured - this is the error message.
60
61
				function $preload:		A function that is used to load data required for this element to be displayed. Must return
62
								true to be displayed at all.
63
64
				string $cast_type:		If set casts the element to a certain type. Valid types (bool, int, float).
65
				string $save_key:		If the index of this element isn't the database column name it can be overriden
66
								with this string.
67
				bool $is_dummy:			If set then nothing is acted upon for this element.
68
				bool $enabled:			A test to determine whether this is even available - if not is unset.
69
				string $link_with:		Key which links this field to an overall set.
70
71
		Note that all elements that have a custom input_validate must ensure they set the value of $cur_profile correct to enable
72
		the changes to be displayed correctly on submit of the form.
73
74
	*/
75
76
	$profile_fields = array(
77
		'avatar_choice' => array(
78
			'type' => 'callback',
79
			'callback_func' => 'avatar_select',
80
			// This handles the permissions too.
81
			'preload' => 'profileLoadAvatarData',
82
			'input_validate' => 'profileSaveAvatarData',
83
			'save_key' => 'avatar',
84
		),
85
		'bday1' => array(
86
			'type' => 'callback',
87
			'callback_func' => 'birthdate',
88
			'permission' => 'profile_extra',
89
			'preload' => function() use ($cur_profile, &$context)
90
			{
91
				// Split up the birthdate....
92
				list ($uyear, $umonth, $uday) = explode('-', empty($cur_profile['birthdate']) || $cur_profile['birthdate'] === '1004-01-01' ? '--' : $cur_profile['birthdate']);
93
				$context['member']['birth_date'] = array(
94
					'year' => $uyear,
95
					'month' => $umonth,
96
					'day' => $uday,
97
				);
98
99
				return true;
100
			},
101
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
102
			{
103
				if (isset($_POST['bday2'], $_POST['bday3']) && $value > 0 && $_POST['bday2'] > 0)
104
				{
105
					// Set to blank?
106
					if ((int) $_POST['bday3'] == 1 && (int) $_POST['bday2'] == 1 && (int) $value == 1)
107
						$value = '1004-01-01';
108
					else
109
						$value = checkdate($value, $_POST['bday2'], $_POST['bday3'] < 1004 ? 1004 : $_POST['bday3']) ? sprintf('%04d-%02d-%02d', $_POST['bday3'] < 1004 ? 1004 : $_POST['bday3'], $_POST['bday1'], $_POST['bday2']) : '1004-01-01';
110
				}
111
				else
112
					$value = '1004-01-01';
113
114
				$profile_vars['birthdate'] = $value;
115
				$cur_profile['birthdate'] = $value;
116
				return false;
117
			},
118
		),
119
		// Setting the birthdate the old style way?
120
		'birthdate' => array(
121
			'type' => 'hidden',
122
			'permission' => 'profile_extra',
123
			'input_validate' => function(&$value) use ($cur_profile)
124
			{
125
				// @todo Should we check for this year and tell them they made a mistake :P? (based on coppa at least?)
126
				if (preg_match('/(\d{4})[\-\., ](\d{2})[\-\., ](\d{2})/', $value, $dates) === 1)
127
				{
128
					$value = checkdate($dates[2], $dates[3], $dates[1] < 4 ? 4 : $dates[1]) ? sprintf('%04d-%02d-%02d', $dates[1] < 4 ? 4 : $dates[1], $dates[2], $dates[3]) : '1004-01-01';
129
					return true;
130
				}
131
				else
132
				{
133
					$value = empty($cur_profile['birthdate']) ? '1004-01-01' : $cur_profile['birthdate'];
134
					return false;
135
				}
136
			},
137
		),
138
		'date_registered' => array(
139
			'type' => 'date',
140
			'value' => empty($cur_profile['date_registered']) ? $txt['not_applicable'] : strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600),
141
			'label' => $txt['date_registered'],
142
			'log_change' => true,
143
			'permission' => 'moderate_forum',
144
			'input_validate' => function(&$value) use ($txt, $user_info, $modSettings, $cur_profile, $context)
0 ignored issues
show
Unused Code introduced by
The import $context is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
145
			{
146
				// Bad date!  Go try again - please?
147
				if (($value = strtotime($value)) === -1)
148
				{
149
					$value = $cur_profile['date_registered'];
150
					return $txt['invalid_registration'] . ' ' . strftime('%d %b %Y ' . (strpos($user_info['time_format'], '%H') !== false ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
151
				}
152
				// As long as it doesn't equal "N/A"...
153
				elseif ($value != $txt['not_applicable'] && $value != strtotime(strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600)))
154
					$value = $value - ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
155
				else
156
					$value = $cur_profile['date_registered'];
157
158
				return true;
159
			},
160
		),
161
		'email_address' => array(
162
			'type' => 'email',
163
			'label' => $txt['user_email_address'],
164
			'subtext' => $txt['valid_email'],
165
			'log_change' => true,
166
			'permission' => 'profile_password',
167
			'js_submit' => !empty($modSettings['send_validation_onChange']) ? '
168
	form_handle.addEventListener(\'submit\', function(event)
169
	{
170
		if (this.email_address.value != "'. (!empty($cur_profile['email_address']) ? $cur_profile['email_address'] : '') . '")
171
		{
172
			alert('. JavaScriptEscape($txt['email_change_logout']) . ');
173
			return true;
174
		}
175
	}, false);' : '',
176
			'input_validate' => function(&$value)
177
			{
178
				global $context, $old_profile, $profile_vars, $sourcedir, $modSettings;
179
180
				if (strtolower($value) == strtolower($old_profile['email_address']))
181
					return false;
182
183
				$isValid = profileValidateEmail($value, $context['id_member']);
184
185
				// Do they need to revalidate? If so schedule the function!
186
				if ($isValid === true && !empty($modSettings['send_validation_onChange']) && !allowedTo('moderate_forum'))
187
				{
188
					require_once($sourcedir . '/Subs-Members.php');
189
					$profile_vars['validation_code'] = generateValidationCode();
190
					$profile_vars['is_activated'] = 2;
191
					$context['profile_execute_on_save'][] = 'profileSendActivation';
192
					unset($context['profile_execute_on_save']['reload_user']);
193
				}
194
195
				return $isValid;
196
			},
197
		),
198
		// Selecting group membership is a complicated one so we treat it separate!
199
		'id_group' => array(
200
			'type' => 'callback',
201
			'callback_func' => 'group_manage',
202
			'permission' => 'manage_membergroups',
203
			'preload' => 'profileLoadGroups',
204
			'log_change' => true,
205
			'input_validate' => 'profileSaveGroups',
206
		),
207
		'id_theme' => array(
208
			'type' => 'callback',
209
			'callback_func' => 'theme_pick',
210
			'permission' => 'profile_extra',
211
			'enabled' => $modSettings['theme_allow'] || allowedTo('admin_forum'),
212
			'preload' => function() use ($smcFunc, &$context, $cur_profile, $txt)
213
			{
214
				$request = $smcFunc['db_query']('', '
215
					SELECT value
216
					FROM {db_prefix}themes
217
					WHERE id_theme = {int:id_theme}
218
						AND variable = {string:variable}
219
					LIMIT 1', array(
220
						'id_theme' => $cur_profile['id_theme'],
221
						'variable' => 'name',
222
					)
223
				);
224
				list ($name) = $smcFunc['db_fetch_row']($request);
225
				$smcFunc['db_free_result']($request);
226
227
				$context['member']['theme'] = array(
228
					'id' => $cur_profile['id_theme'],
229
					'name' => empty($cur_profile['id_theme']) ? $txt['theme_forum_default'] : $name
230
				);
231
				return true;
232
			},
233
			'input_validate' => function(&$value)
234
			{
235
				$value = (int) $value;
236
				return true;
237
			},
238
		),
239
		'lngfile' => array(
240
			'type' => 'select',
241
			'options' => function() use (&$context)
242
			{
243
				return $context['profile_languages'];
244
			},
245
			'label' => $txt['preferred_language'],
246
			'permission' => 'profile_identity',
247
			'preload' => 'profileLoadLanguages',
248
			'enabled' => !empty($modSettings['userLanguage']),
249
			'value' => empty($cur_profile['lngfile']) ? $language : $cur_profile['lngfile'],
250
			'input_validate' => function(&$value) use (&$context, $cur_profile)
251
			{
252
				// Load the languages.
253
				profileLoadLanguages();
254
255
				if (isset($context['profile_languages'][$value]))
256
				{
257
					if ($context['user']['is_owner'] && empty($context['password_auth_failed']))
258
						$_SESSION['language'] = $value;
259
					return true;
260
				}
261
				else
262
				{
263
					$value = $cur_profile['lngfile'];
264
					return false;
265
				}
266
			},
267
		),
268
		// The username is not always editable - so adjust it as such.
269
		'member_name' => array(
270
			'type' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? 'text' : 'label',
271
			'label' => $txt['username'],
272
			'subtext' => allowedTo('admin_forum') && !isset($_GET['changeusername']) ? '[<a href="' . $scripturl . '?action=profile;u=' . $context['id_member'] . ';area=account;changeusername" style="font-style: italic;">' . $txt['username_change'] . '</a>]' : '',
273
			'log_change' => true,
274
			'permission' => 'profile_identity',
275
			'prehtml' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? '<div class="alert">' . $txt['username_warning'] . '</div>' : '',
276
			'input_validate' => function(&$value) use ($sourcedir, $context, $user_info, $cur_profile)
277
			{
278
				if (allowedTo('admin_forum'))
279
				{
280
					// We'll need this...
281
					require_once($sourcedir . '/Subs-Auth.php');
282
283
					// Maybe they are trying to change their password as well?
284
					$resetPassword = true;
285
					if (isset($_POST['passwrd1']) && $_POST['passwrd1'] != '' && isset($_POST['passwrd2']) && $_POST['passwrd1'] == $_POST['passwrd2'] && validatePassword($_POST['passwrd1'], $value, array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email'])) == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing validatePassword($_POST[..., $user_info['email'])) of type null|string 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)
0 ignored issues
show
Unused Code introduced by
The import $smcFunc is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
313
			{
314
				// If we didn't try it then ignore it!
315
				if ($value == '')
316
					return false;
317
318
				// Do the two entries for the password even match?
319
				if (!isset($_POST['passwrd2']) || $value != $_POST['passwrd2'])
320
					return 'bad_new_password';
321
322
				// Let's get the validation function into play...
323
				require_once($sourcedir . '/Subs-Auth.php');
324
				$passwordErrors = validatePassword($value, $cur_profile['member_name'], array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email']));
325
326
				// Were there errors?
327
				if ($passwordErrors != null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $passwordErrors of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
328
					return 'password_' . $passwordErrors;
329
330
				// Set up the new password variable... ready for storage.
331
				$value = hash_password($cur_profile['member_name'], un_htmlspecialchars($value));
332
333
				return true;
334
			},
335
		),
336
		'passwrd2' => array(
337
			'type' => 'password',
338
			'label' => ucwords($txt['verify_pass']),
339
			'size' => 20,
340
			'value' => '',
341
			'permission' => 'profile_password',
342
			'is_dummy' => true,
343
		),
344
		'personal_text' => array(
345
			'type' => 'text',
346
			'label' => $txt['personal_text'],
347
			'log_change' => true,
348
			'input_attr' => array('maxlength="50"'),
349
			'size' => 50,
350
			'permission' => 'profile_blurb',
351
			'input_validate' => function(&$value) use ($smcFunc)
352
			{
353
				if ($smcFunc['strlen']($value) > 50)
354
					return 'personal_text_too_long';
355
356
				return true;
357
			},
358
		),
359
		// This does ALL the pm settings
360
		'pm_prefs' => array(
361
			'type' => 'callback',
362
			'callback_func' => 'pm_settings',
363
			'permission' => 'pm_read',
364
			'preload' => function() use (&$context, $cur_profile)
365
			{
366
				$context['display_mode'] = $cur_profile['pm_prefs'] & 3;
367
				$context['receive_from'] = !empty($cur_profile['pm_receive_from']) ? $cur_profile['pm_receive_from'] : 0;
368
369
				return true;
370
			},
371
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
372
			{
373
				// Simple validate and apply the two "sub settings"
374
				$value = max(min($value, 2), 0);
375
376
				$cur_profile['pm_receive_from'] = $profile_vars['pm_receive_from'] = max(min((int) $_POST['pm_receive_from'], 4), 0);
377
378
				return true;
379
			},
380
		),
381
		'posts' => array(
382
			'type' => 'int',
383
			'label' => $txt['profile_posts'],
384
			'log_change' => true,
385
			'size' => 7,
386
			'permission' => 'moderate_forum',
387
			'input_validate' => function(&$value)
388
			{
389
				if (!is_numeric($value))
390
					return 'digits_only';
391
				else
392
					$value = $value != '' ? strtr($value, array(',' => '', '.' => '', ' ' => '')) : 0;
393
				return true;
394
			},
395
		),
396
		'real_name' => array(
397
			'type' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum') ? 'text' : 'label',
398
			'label' => $txt['name'],
399
			'subtext' => $txt['display_name_desc'],
400
			'log_change' => true,
401
			'input_attr' => array('maxlength="60"'),
402
			'permission' => 'profile_displayed_name',
403
			'enabled' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum'),
404
			'input_validate' => function(&$value) use ($context, $smcFunc, $sourcedir, $cur_profile)
405
			{
406
				$value = trim(preg_replace('~[\t\n\r \x0B\0' . ($context['utf8'] ? '\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}' : '\x00-\x08\x0B\x0C\x0E-\x19\xA0') . ']+~' . ($context['utf8'] ? 'u' : ''), ' ', $value));
407
408
				if (trim($value) == '')
409
					return 'no_name';
410
				elseif ($smcFunc['strlen']($value) > 60)
411
					return 'name_too_long';
412
				elseif ($cur_profile['real_name'] != $value)
413
				{
414
					require_once($sourcedir . '/Subs-Members.php');
415
					if (isReservedName($value, $context['id_member']))
416
						return 'name_taken';
417
				}
418
				return true;
419
			},
420
		),
421
		'secret_question' => array(
422
			'type' => 'text',
423
			'label' => $txt['secret_question'],
424
			'subtext' => $txt['secret_desc'],
425
			'size' => 50,
426
			'permission' => 'profile_password',
427
		),
428
		'secret_answer' => array(
429
			'type' => 'text',
430
			'label' => $txt['secret_answer'],
431
			'subtext' => $txt['secret_desc2'],
432
			'size' => 20,
433
			'postinput' => '<span class="smalltext"><a href="' . $scripturl . '?action=helpadmin;help=secret_why_blank" onclick="return reqOverlayDiv(this.href);"><span class="generic_icons help"></span> ' . $txt['secret_why_blank'] . '</a></span>',
434
			'value' => '',
435
			'permission' => 'profile_password',
436
			'input_validate' => function(&$value) use ($cur_profile)
437
			{
438
				$value = $value != '' ? hash_password($cur_profile['member_name'], $value) : '';
439
				return true;
440
			},
441
		),
442
		'signature' => array(
443
			'type' => 'callback',
444
			'callback_func' => 'signature_modify',
445
			'permission' => 'profile_signature',
446
			'enabled' => substr($modSettings['signature_settings'], 0, 1) == 1,
447
			'preload' => 'profileLoadSignatureData',
448
			'input_validate' => 'profileValidateSignature',
449
		),
450
		'show_online' => array(
451
			'type' => 'check',
452
			'label' => $txt['show_online'],
453
			'permission' => 'profile_identity',
454
			'enabled' => !empty($modSettings['allow_hideOnline']) || allowedTo('moderate_forum'),
455
		),
456
		'smiley_set' => array(
457
			'type' => 'callback',
458
			'callback_func' => 'smiley_pick',
459
			'enabled' => !empty($modSettings['smiley_sets_enable']),
460
			'permission' => 'profile_extra',
461
			'preload' => function() use ($modSettings, &$context, $txt, $cur_profile, $smcFunc)
462
			{
463
				$context['member']['smiley_set']['id'] = empty($cur_profile['smiley_set']) ? '' : $cur_profile['smiley_set'];
464
				$context['smiley_sets'] = explode(',', 'none,,' . $modSettings['smiley_sets_known']);
465
				$set_names = explode("\n", $txt['smileys_none'] . "\n" . $txt['smileys_forum_board_default'] . "\n" . $modSettings['smiley_sets_names']);
466
				foreach ($context['smiley_sets'] as $i => $set)
467
				{
468
					$context['smiley_sets'][$i] = array(
469
						'id' => $smcFunc['htmlspecialchars']($set),
470
						'name' => $smcFunc['htmlspecialchars']($set_names[$i]),
471
						'selected' => $set == $context['member']['smiley_set']['id']
472
					);
473
474
					if ($context['smiley_sets'][$i]['selected'])
475
						$context['member']['smiley_set']['name'] = $set_names[$i];
476
				}
477
				return true;
478
			},
479
			'input_validate' => function(&$value)
480
			{
481
				global $modSettings;
482
483
				$smiley_sets = explode(',', $modSettings['smiley_sets_known']);
484
				if (!in_array($value, $smiley_sets) && $value != 'none')
485
					$value = '';
486
				return true;
487
			},
488
		),
489
		// Pretty much a dummy entry - it populates all the theme settings.
490
		'theme_settings' => array(
491
			'type' => 'callback',
492
			'callback_func' => 'theme_settings',
493
			'permission' => 'profile_extra',
494
			'is_dummy' => true,
495
			'preload' => function() use (&$context, $user_info, $modSettings)
496
			{
497
				loadLanguage('Settings');
498
499
				$context['allow_no_censored'] = false;
500
				if ($user_info['is_admin'] || $context['user']['is_owner'])
501
					$context['allow_no_censored'] = !empty($modSettings['allow_no_censored']);
502
503
				return true;
504
			},
505
		),
506
		'tfa' => array(
507
			'type' => 'callback',
508
			'callback_func' => 'tfa',
509
			'permission' => 'profile_password',
510
			'enabled' => !empty($modSettings['tfa_mode']),
511
			'preload' => function() use (&$context, $cur_profile)
512
			{
513
				$context['tfa_enabled'] = !empty($cur_profile['tfa_secret']);
514
515
				return true;
516
			},
517
		),
518
		'time_format' => array(
519
			'type' => 'callback',
520
			'callback_func' => 'timeformat_modify',
521
			'permission' => 'profile_extra',
522
			'preload' => function() use (&$context, $user_info, $txt, $cur_profile, $modSettings)
523
			{
524
				$context['easy_timeformats'] = array(
525
					array('format' => '', 'title' => $txt['timeformat_default']),
526
					array('format' => '%B %d, %Y, %I:%M:%S %p', 'title' => $txt['timeformat_easy1']),
527
					array('format' => '%B %d, %Y, %H:%M:%S', 'title' => $txt['timeformat_easy2']),
528
					array('format' => '%Y-%m-%d, %H:%M:%S', 'title' => $txt['timeformat_easy3']),
529
					array('format' => '%d %B %Y, %H:%M:%S', 'title' => $txt['timeformat_easy4']),
530
					array('format' => '%d-%m-%Y, %H:%M:%S', 'title' => $txt['timeformat_easy5'])
531
				);
532
533
				$context['member']['time_format'] = $cur_profile['time_format'];
534
				$context['current_forum_time'] = timeformat(time() - $user_info['time_offset'] * 3600, false);
535
				$context['current_forum_time_js'] = strftime('%Y,' . ((int) strftime('%m', time() + $modSettings['time_offset'] * 3600) - 1) . ',%d,%H,%M,%S', time() + $modSettings['time_offset'] * 3600);
536
				$context['current_forum_time_hour'] = (int) strftime('%H', forum_time(false));
537
				return true;
538
			},
539
		),
540
		'timezone' => array(
541
			'type' => 'select',
542
			'options' => smf_list_timezones(),
543
			'disabled_options' => array_filter(array_keys(smf_list_timezones()), 'is_int'),
544
			'permission' => 'profile_extra',
545
			'label' => $txt['timezone'],
546
			'input_validate' => function($value)
547
			{
548
				$tz = smf_list_timezones();
549
				if (!isset($tz[$value]))
550
					return 'bad_timezone';
551
552
				return true;
553
			},
554
		),
555
		'usertitle' => array(
556
			'type' => 'text',
557
			'label' => $txt['custom_title'],
558
			'log_change' => true,
559
			'input_attr' => array('maxlength="50"'),
560
			'size' => 50,
561
			'permission' => 'profile_title',
562
			'enabled' => !empty($modSettings['titlesEnable']),
563
			'input_validate' => function(&$value) use ($smcFunc)
564
			{
565
				if ($smcFunc['strlen']($value) > 50)
566
					return 'user_title_too_long';
567
568
				return true;
569
			},
570
		),
571
		'website_title' => array(
572
			'type' => 'text',
573
			'label' => $txt['website_title'],
574
			'subtext' => $txt['include_website_url'],
575
			'size' => 50,
576
			'permission' => 'profile_website',
577
			'link_with' => 'website',
578
		),
579
		'website_url' => array(
580
			'type' => 'url',
581
			'label' => $txt['website_url'],
582
			'subtext' => $txt['complete_url'],
583
			'size' => 50,
584
			'permission' => 'profile_website',
585
			// Fix the URL...
586
			'input_validate' => function(&$value)
587
			{
588
				if (strlen(trim($value)) > 0 && strpos($value, '://') === false)
589
					$value = 'http://' . $value;
590
				if (strlen($value) < 8 || (substr($value, 0, 7) !== 'http://' && substr($value, 0, 8) !== 'https://'))
591
					$value = '';
592
				$value = (string) validate_iri(sanitize_iri($value));
593
				return true;
594
			},
595
			'link_with' => 'website',
596
		),
597
	);
598
599
	call_integration_hook('integrate_load_profile_fields', array(&$profile_fields));
600
601
	$disabled_fields = !empty($modSettings['disabled_profile_fields']) ? explode(',', $modSettings['disabled_profile_fields']) : array();
602
	// For each of the above let's take out the bits which don't apply - to save memory and security!
603
	foreach ($profile_fields as $key => $field)
604
	{
605
		// Do we have permission to do this?
606
		if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? array($field['permission'] . '_own', $field['permission'] . '_any') : $field['permission'] . '_any')) && !allowedTo($field['permission']))
607
			unset($profile_fields[$key]);
608
609
		// Is it enabled?
610
		if (isset($field['enabled']) && !$field['enabled'])
611
			unset($profile_fields[$key]);
612
613
		// Is it specifically disabled?
614
		if (in_array($key, $disabled_fields) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields)))
615
			unset($profile_fields[$key]);
616
	}
617
}
618
619
/**
620
 * Setup the context for a page load!
621
 *
622
 * @param array $fields The profile fields to display. Each item should correspond to an item in the $profile_fields array generated by loadProfileFields
623
 */
624
function setupProfileContext($fields)
625
{
626
	global $profile_fields, $context, $cur_profile, $txt;
627
628
	// Some default bits.
629
	$context['profile_prehtml'] = '';
630
	$context['profile_posthtml'] = '';
631
	$context['profile_javascript'] = '';
632
	$context['profile_onsubmit_javascript'] = '';
633
634
	call_integration_hook('integrate_setup_profile_context', array(&$fields));
635
636
	// Make sure we have this!
637
	loadProfileFields(true);
638
639
	// First check for any linked sets.
640
	foreach ($profile_fields as $key => $field)
641
		if (isset($field['link_with']) && in_array($field['link_with'], $fields))
642
			$fields[] = $key;
643
644
	$i = 0;
645
	$last_type = '';
646
	foreach ($fields as $key => $field)
647
	{
648
		if (isset($profile_fields[$field]))
649
		{
650
			// Shortcut.
651
			$cur_field = &$profile_fields[$field];
652
653
			// Does it have a preload and does that preload succeed?
654
			if (isset($cur_field['preload']) && !$cur_field['preload']())
655
				continue;
656
657
			// If this is anything but complex we need to do more cleaning!
658
			if ($cur_field['type'] != 'callback' && $cur_field['type'] != 'hidden')
659
			{
660
				if (!isset($cur_field['label']))
661
					$cur_field['label'] = isset($txt[$field]) ? $txt[$field] : $field;
662
663
				// Everything has a value!
664
				if (!isset($cur_field['value']))
665
					$cur_field['value'] = isset($cur_profile[$field]) ? $cur_profile[$field] : '';
666
667
				// Any input attributes?
668
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
669
			}
670
671
			// Was there an error with this field on posting?
672
			if (isset($context['profile_errors'][$field]))
673
				$cur_field['is_error'] = true;
674
675
			// Any javascript stuff?
676
			if (!empty($cur_field['js_submit']))
677
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
678
			if (!empty($cur_field['js']))
679
				$context['profile_javascript'] .= $cur_field['js'];
680
681
			// Any template stuff?
682
			if (!empty($cur_field['prehtml']))
683
				$context['profile_prehtml'] .= $cur_field['prehtml'];
684
			if (!empty($cur_field['posthtml']))
685
				$context['profile_posthtml'] .= $cur_field['posthtml'];
686
687
			// Finally put it into context?
688
			if ($cur_field['type'] != 'hidden')
689
			{
690
				$last_type = $cur_field['type'];
691
				$context['profile_fields'][$field] = &$profile_fields[$field];
692
			}
693
		}
694
		// Bodge in a line break - without doing two in a row ;)
695
		elseif ($field == 'hr' && $last_type != 'hr' && $last_type != '')
696
		{
697
			$last_type = 'hr';
698
			$context['profile_fields'][$i++]['type'] = 'hr';
699
		}
700
	}
701
702
	// Some spicy JS.
703
	addInlineJavaScript('
704
	var form_handle = document.forms.creator;
705
	createEventListener(form_handle);
706
	'. (!empty($context['require_password']) ? '
707
	form_handle.addEventListener(\'submit\', function(event)
708
	{
709
		if (this.oldpasswrd.value == "")
710
		{
711
			event.preventDefault();
712
			alert('. (JavaScriptEscape($txt['required_security_reasons'])) . ');
713
			return false;
714
		}
715
	}, false);' : ''), true);
716
717
	// Any onsubmit javascript?
718
	if (!empty($context['profile_onsubmit_javascript']))
719
		addInlineJavaScript($context['profile_onsubmit_javascript'], true);
720
721
	// Any totally custom stuff?
722
	if (!empty($context['profile_javascript']))
723
		addInlineJavaScript($context['profile_javascript'], true);
724
725
	// Free up some memory.
726
	unset($profile_fields);
727
}
728
729
/**
730
 * Save the profile changes.
731
 */
732
function saveProfileFields()
733
{
734
	global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile;
735
736
	// Load them up.
737
	loadProfileFields();
738
739
	// This makes things easier...
740
	$old_profile = $cur_profile;
741
742
	// This allows variables to call activities when they save - by default just to reload their settings
743
	$context['profile_execute_on_save'] = array();
744
	if ($context['user']['is_owner'])
745
		$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
746
747
	// Assume we log nothing.
748
	$context['log_changes'] = array();
749
750
	// Cycle through the profile fields working out what to do!
751
	foreach ($profile_fields as $key => $field)
752
	{
753
		if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key == 'signature'))
754
			continue;
755
756
		// What gets updated?
757
		$db_key = isset($field['save_key']) ? $field['save_key'] : $key;
758
759
		// Right - we have something that is enabled, we can act upon and has a value posted to it. Does it have a validation function?
760
		if (isset($field['input_validate']))
761
		{
762
			$is_valid = $field['input_validate']($_POST[$key]);
763
			// An error occurred - set it as such!
764
			if ($is_valid !== true)
765
			{
766
				// Is this an actual error?
767
				if ($is_valid !== false)
768
				{
769
					$post_errors[$key] = $is_valid;
770
					$profile_fields[$key]['is_error'] = $is_valid;
771
				}
772
				// Retain the old value.
773
				$cur_profile[$key] = $_POST[$key];
774
				continue;
775
			}
776
		}
777
778
		// Are we doing a cast?
779
		$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
780
781
		// Finally, clean up certain types.
782
		if ($field['cast_type'] == 'int')
783
			$_POST[$key] = (int) $_POST[$key];
784
		elseif ($field['cast_type'] == 'float')
785
			$_POST[$key] = (float) $_POST[$key];
786
		elseif ($field['cast_type'] == 'check')
787
			$_POST[$key] = !empty($_POST[$key]) ? 1 : 0;
788
789
		// If we got here we're doing OK.
790
		if ($field['type'] != 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
791
		{
792
			// Set the save variable.
793
			$profile_vars[$db_key] = $_POST[$key];
794
			// And update the user profile.
795
			$cur_profile[$key] = $_POST[$key];
796
797
			// Are we logging it?
798
			if (!empty($field['log_change']) && isset($old_profile[$key]))
799
				$context['log_changes'][$key] = array(
800
					'previous' => $old_profile[$key],
801
					'new' => $_POST[$key],
802
				);
803
		}
804
805
		// Logging group changes are a bit different...
806
		if ($key == 'id_group' && $field['log_change'])
807
		{
808
			profileLoadGroups();
809
810
			// Any changes to primary group?
811
			if ($_POST['id_group'] != $old_profile['id_group'])
812
			{
813
				$context['log_changes']['id_group'] = array(
814
					'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
815
					'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
816
				);
817
			}
818
819
			// Prepare additional groups for comparison.
820
			$additional_groups = array(
821
				'previous' => !empty($old_profile['additional_groups']) ? explode(',', $old_profile['additional_groups']) : array(),
822
				'new' => !empty($_POST['additional_groups']) ? array_diff($_POST['additional_groups'], array(0)) : array(),
823
			);
824
825
			sort($additional_groups['previous']);
826
			sort($additional_groups['new']);
827
828
			// What about additional groups?
829
			if ($additional_groups['previous'] != $additional_groups['new'])
830
			{
831
				foreach ($additional_groups as $type => $groups)
832
				{
833
					foreach ($groups as $id => $group)
834
					{
835
						if (isset($context['member_groups'][$group]))
836
							$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
837
						else
838
							unset($additional_groups[$type][$id]);
839
					}
840
					$additional_groups[$type] = implode(', ', $additional_groups[$type]);
841
				}
842
843
				$context['log_changes']['additional_groups'] = $additional_groups;
844
			}
845
		}
846
	}
847
848
	// @todo Temporary
849
	if ($context['user']['is_owner'])
850
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
851
	else
852
		$changeOther = allowedTo('profile_extra_any');
853
	if ($changeOther && empty($post_errors))
854
	{
855
		makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
856
		if (!empty($_REQUEST['sa']))
857
		{
858
			$custom_fields_errors = makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false, true);
859
860
			if (!empty($custom_fields_errors))
861
				$post_errors = array_merge($post_errors, $custom_fields_errors);
862
		}
863
	}
864
865
	// Free memory!
866
	unset($profile_fields);
867
}
868
869
/**
870
 * Save the profile changes
871
 *
872
 * @param array &$profile_vars The items to save
873
 * @param array &$post_errors An array of information about any errors that occurred
874
 * @param int $memID The ID of the member whose profile we're saving
875
 */
876
function saveProfileChanges(&$profile_vars, &$post_errors, $memID)
877
{
878
	global $user_profile, $context;
879
880
	// These make life easier....
881
	$old_profile = &$user_profile[$memID];
882
883
	// Permissions...
884
	if ($context['user']['is_owner'])
885
	{
886
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own', 'profile_website_any', 'profile_website_own', 'profile_signature_any', 'profile_signature_own'));
887
	}
888
	else
889
		$changeOther = allowedTo(array('profile_extra_any', 'profile_website_any', 'profile_signature_any'));
890
891
	// Arrays of all the changes - makes things easier.
892
	$profile_bools = array();
893
	$profile_ints = array();
894
	$profile_floats = array();
895
	$profile_strings = array(
896
		'buddy_list',
897
		'ignore_boards',
898
	);
899
900
	if (isset($_POST['sa']) && $_POST['sa'] == 'ignoreboards' && empty($_POST['ignore_brd']))
901
		$_POST['ignore_brd'] = array();
902
903
	unset($_POST['ignore_boards']); // Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
904
	if (isset($_POST['ignore_brd']))
905
	{
906
		if (!is_array($_POST['ignore_brd']))
907
			$_POST['ignore_brd'] = array($_POST['ignore_brd']);
908
909
		foreach ($_POST['ignore_brd'] as $k => $d)
910
		{
911
			$d = (int) $d;
912
			if ($d != 0)
913
				$_POST['ignore_brd'][$k] = $d;
914
			else
915
				unset($_POST['ignore_brd'][$k]);
916
		}
917
		$_POST['ignore_boards'] = implode(',', $_POST['ignore_brd']);
918
		unset($_POST['ignore_brd']);
919
	}
920
921
	// Here's where we sort out all the 'other' values...
922
	if ($changeOther)
923
	{
924
		makeThemeChanges($memID, isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
925
		//makeAvatarChanges($memID, $post_errors);
926
927
		if (!empty($_REQUEST['sa']))
928
			makeCustomFieldChanges($memID, $_REQUEST['sa'], false);
929
930
		foreach ($profile_bools as $var)
931
			if (isset($_POST[$var]))
932
				$profile_vars[$var] = empty($_POST[$var]) ? '0' : '1';
933
		foreach ($profile_ints as $var)
934
			if (isset($_POST[$var]))
935
				$profile_vars[$var] = $_POST[$var] != '' ? (int) $_POST[$var] : '';
936
		foreach ($profile_floats as $var)
937
			if (isset($_POST[$var]))
938
				$profile_vars[$var] = (float) $_POST[$var];
939
		foreach ($profile_strings as $var)
940
			if (isset($_POST[$var]))
941
				$profile_vars[$var] = $_POST[$var];
942
	}
943
}
944
945
/**
946
 * Make any theme changes that are sent with the profile.
947
 *
948
 * @param int $memID The ID of the user
949
 * @param int $id_theme The ID of the theme
950
 */
951
function makeThemeChanges($memID, $id_theme)
952
{
953
	global $modSettings, $smcFunc, $context, $user_info;
954
955
	$reservedVars = array(
956
		'actual_theme_url',
957
		'actual_images_url',
958
		'base_theme_dir',
959
		'base_theme_url',
960
		'default_images_url',
961
		'default_theme_dir',
962
		'default_theme_url',
963
		'default_template',
964
		'images_url',
965
		'number_recent_posts',
966
		'smiley_sets_default',
967
		'theme_dir',
968
		'theme_id',
969
		'theme_layers',
970
		'theme_templates',
971
		'theme_url',
972
	);
973
974
	// Can't change reserved vars.
975
	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))
976
		fatal_lang_error('no_access', false);
977
978
	// Don't allow any overriding of custom fields with default or non-default options.
979
	$request = $smcFunc['db_query']('', '
980
		SELECT col_name
981
		FROM {db_prefix}custom_fields
982
		WHERE active = {int:is_active}',
983
		array(
984
			'is_active' => 1,
985
		)
986
	);
987
	$custom_fields = array();
988
	while ($row = $smcFunc['db_fetch_assoc']($request))
989
		$custom_fields[] = $row['col_name'];
990
	$smcFunc['db_free_result']($request);
991
992
	// These are the theme changes...
993
	$themeSetArray = array();
994
	if (isset($_POST['options']) && is_array($_POST['options']))
995
	{
996
		foreach ($_POST['options'] as $opt => $val)
997
		{
998
			if (in_array($opt, $custom_fields))
999
				continue;
1000
1001
			// These need to be controlled.
1002
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1003
				$val = max(0, min($val, 50));
1004
			// We don't set this per theme anymore.
1005
			elseif ($opt == 'allow_no_censored')
1006
				continue;
1007
1008
			$themeSetArray[] = array($memID, $id_theme, $opt, is_array($val) ? implode(',', $val) : $val);
1009
		}
1010
	}
1011
1012
	$erase_options = array();
1013
	if (isset($_POST['default_options']) && is_array($_POST['default_options']))
1014
		foreach ($_POST['default_options'] as $opt => $val)
1015
		{
1016
			if (in_array($opt, $custom_fields))
1017
				continue;
1018
1019
			// These need to be controlled.
1020
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1021
				$val = max(0, min($val, 50));
1022
			// Only let admins and owners change the censor.
1023
			elseif ($opt == 'allow_no_censored' && !$user_info['is_admin'] && !$context['user']['is_owner'])
1024
					continue;
1025
1026
			$themeSetArray[] = array($memID, 1, $opt, is_array($val) ? implode(',', $val) : $val);
1027
			$erase_options[] = $opt;
1028
		}
1029
1030
	// If themeSetArray isn't still empty, send it to the database.
1031
	if (empty($context['password_auth_failed']))
1032
	{
1033
		if (!empty($themeSetArray))
1034
		{
1035
			$smcFunc['db_insert']('replace',
1036
				'{db_prefix}themes',
1037
				array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
1038
				$themeSetArray,
1039
				array('id_member', 'id_theme', 'variable')
1040
			);
1041
		}
1042
1043
		if (!empty($erase_options))
1044
		{
1045
			$smcFunc['db_query']('', '
1046
				DELETE FROM {db_prefix}themes
1047
				WHERE id_theme != {int:id_theme}
1048
					AND variable IN ({array_string:erase_variables})
1049
					AND id_member = {int:id_member}',
1050
				array(
1051
					'id_theme' => 1,
1052
					'id_member' => $memID,
1053
					'erase_variables' => $erase_options
1054
				)
1055
			);
1056
		}
1057
1058
		// Admins can choose any theme, even if it's not enabled...
1059
		$themes = allowedTo('admin_forum') ? explode(',', $modSettings['knownThemes']) : explode(',', $modSettings['enableThemes']);
1060
		foreach ($themes as $t)
1061
			cache_put_data('theme_settings-' . $t . ':' . $memID, null, 60);
1062
	}
1063
}
1064
1065
/**
1066
 * Make any notification changes that need to be made.
1067
 *
1068
 * @param int $memID The ID of the member
1069
 */
1070
function makeNotificationChanges($memID)
1071
{
1072
	global $smcFunc, $sourcedir;
1073
1074
	require_once($sourcedir . '/Subs-Notify.php');
1075
1076
	// Update the boards they are being notified on.
1077
	if (isset($_POST['edit_notify_boards']) && !empty($_POST['notify_boards']))
1078
	{
1079
		// Make sure only integers are deleted.
1080
		foreach ($_POST['notify_boards'] as $index => $id)
1081
			$_POST['notify_boards'][$index] = (int) $id;
1082
1083
		// id_board = 0 is reserved for topic notifications.
1084
		$_POST['notify_boards'] = array_diff($_POST['notify_boards'], array(0));
1085
1086
		$smcFunc['db_query']('', '
1087
			DELETE FROM {db_prefix}log_notify
1088
			WHERE id_board IN ({array_int:board_list})
1089
				AND id_member = {int:selected_member}',
1090
			array(
1091
				'board_list' => $_POST['notify_boards'],
1092
				'selected_member' => $memID,
1093
			)
1094
		);
1095
	}
1096
1097
	// We are editing topic notifications......
1098
	elseif (isset($_POST['edit_notify_topics']) && !empty($_POST['notify_topics']))
1099
	{
1100
		foreach ($_POST['notify_topics'] as $index => $id)
1101
			$_POST['notify_topics'][$index] = (int) $id;
1102
1103
		// Make sure there are no zeros left.
1104
		$_POST['notify_topics'] = array_diff($_POST['notify_topics'], array(0));
1105
1106
		$smcFunc['db_query']('', '
1107
			DELETE FROM {db_prefix}log_notify
1108
			WHERE id_topic IN ({array_int:topic_list})
1109
				AND id_member = {int:selected_member}',
1110
			array(
1111
				'topic_list' => $_POST['notify_topics'],
1112
				'selected_member' => $memID,
1113
			)
1114
		);
1115
		foreach ($_POST['notify_topics'] as $topic)
1116
			setNotifyPrefs($memID, array('topic_notify_' . $topic => 0));
1117
	}
1118
1119
	// We are removing topic preferences
1120
	elseif (isset($_POST['remove_notify_topics']) && !empty($_POST['notify_topics']))
1121
	{
1122
		$prefs = array();
1123
		foreach ($_POST['notify_topics'] as $topic)
1124
			$prefs[] = 'topic_notify_' . $topic;
1125
		deleteNotifyPrefs($memID, $prefs);
1126
	}
1127
1128
	// We are removing board preferences
1129
	elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
1130
	{
1131
		$prefs = array();
1132
		foreach ($_POST['notify_boards'] as $board)
1133
			$prefs[] = 'board_notify_' . $board;
1134
		deleteNotifyPrefs($memID, $prefs);
1135
	}
1136
}
1137
1138
/**
1139
 * Save any changes to the custom profile fields
1140
 *
1141
 * @param int $memID The ID of the member
1142
 * @param string $area The area of the profile these fields are in
1143
 * @param bool $sanitize = true Whether or not to sanitize the data
1144
 * @param bool $returnErrors Whether or not to return any error information
1145
 * @return void|array Returns nothing or returns an array of error info if $returnErrors is true
1146
 */
1147
function makeCustomFieldChanges($memID, $area, $sanitize = true, $returnErrors = false)
1148
{
1149
	global $context, $smcFunc, $user_profile, $user_info, $modSettings;
1150
	global $sourcedir;
1151
1152
	$errors = array();
1153
1154
	if ($sanitize && isset($_POST['customfield']))
1155
		$_POST['customfield'] = htmlspecialchars__recursive($_POST['customfield']);
1156
1157
	$where = $area == 'register' ? 'show_reg != 0' : 'show_profile = {string:area}';
1158
1159
	// Load the fields we are saving too - make sure we save valid data (etc).
1160
	$request = $smcFunc['db_query']('', '
1161
		SELECT col_name, field_name, field_desc, field_type, field_length, field_options, default_value, show_reg, mask, private
1162
		FROM {db_prefix}custom_fields
1163
		WHERE ' . $where . '
1164
			AND active = {int:is_active}',
1165
		array(
1166
			'is_active' => 1,
1167
			'area' => $area,
1168
		)
1169
	);
1170
	$changes = array();
1171
	$deletes = array();
1172
	$log_changes = array();
1173
	while ($row = $smcFunc['db_fetch_assoc']($request))
1174
	{
1175
		/* This means don't save if:
1176
			- The user is NOT an admin.
1177
			- The data is not freely viewable and editable by users.
1178
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
1179
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
1180
		*/
1181
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != $user_info['id'] || $row['private'] != 2) && ($area != 'register' || $row['show_reg'] == 0))
1182
			continue;
1183
1184
		// Validate the user data.
1185
		if ($row['field_type'] == 'check')
1186
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
1187
		elseif ($row['field_type'] == 'select' || $row['field_type'] == 'radio')
1188
		{
1189
			$value = $row['default_value'];
1190
			foreach (explode(',', $row['field_options']) as $k => $v)
1191
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
1192
					$value = $v;
1193
		}
1194
		// Otherwise some form of text!
1195
		else
1196
		{
1197
			$value = isset($_POST['customfield'][$row['col_name']]) ? $_POST['customfield'][$row['col_name']] : '';
1198
1199
			if ($row['field_length'])
1200
				$value = $smcFunc['substr']($value, 0, $row['field_length']);
1201
1202
			// Any masks?
1203
			if ($row['field_type'] == 'text' && !empty($row['mask']) && $row['mask'] != 'none')
1204
			{
1205
				$value = $smcFunc['htmltrim']($value);
1206
				$valueReference = un_htmlspecialchars($value);
1207
1208
				// Try and avoid some checks. '0' could be a valid non-empty value.
1209
				if (empty($value) && !is_numeric($value))
1210
					$value = '';
1211
1212
				if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $value != filter_var($value, FILTER_SANITIZE_STRING) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
1213
				{
1214
					if ($returnErrors)
1215
						$errors[] = 'custom_field_nohtml_fail';
1216
1217
					else
1218
						$value = '';
1219
				}
1220
				elseif ($row['mask'] == 'email' && (!filter_var($value, FILTER_VALIDATE_EMAIL) || strlen($value) > 255))
1221
				{
1222
					if ($returnErrors)
1223
						$errors[] = 'custom_field_mail_fail';
1224
1225
					else
1226
						$value = '';
1227
				}
1228
				elseif ($row['mask'] == 'number')
1229
				{
1230
					$value = (int) $value;
1231
				}
1232
				elseif (substr($row['mask'], 0, 5) == 'regex' && trim($value) != '' && preg_match(substr($row['mask'], 5), $value) === 0)
1233
				{
1234
					if ($returnErrors)
1235
						$errors[] = 'custom_field_regex_fail';
1236
1237
					else
1238
						$value = '';
1239
				}
1240
1241
				unset($valueReference);
1242
			}
1243
		}
1244
1245
		// Did it change?
1246
		if (!isset($user_profile[$memID]['options'][$row['col_name']]) || $user_profile[$memID]['options'][$row['col_name']] !== $value)
1247
		{
1248
			$log_changes[] = array(
1249
				'action' => 'customfield_' . $row['col_name'],
1250
				'log_type' => 'user',
1251
				'extra' => array(
1252
					'previous' => !empty($user_profile[$memID]['options'][$row['col_name']]) ? $user_profile[$memID]['options'][$row['col_name']] : '',
1253
					'new' => $value,
1254
					'applicator' => $user_info['id'],
1255
					'member_affected' => $memID,
1256
				),
1257
			);
1258
			if (empty($value))
1259
			{
1260
				$deletes = array('id_theme' => 1 , 'variable' => $row['col_name'], 'id_member' => $memID);
1261
				unset($user_profile[$memID]['options'][$row['col_name']]);
1262
			}
1263
			else
1264
			{
1265
				$changes[] = array(1, $row['col_name'], $value, $memID);
1266
				$user_profile[$memID]['options'][$row['col_name']] = $value;
1267
			}
1268
		}
1269
	}
1270
	$smcFunc['db_free_result']($request);
1271
1272
	$hook_errors = call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, &$errors, $returnErrors, $memID, $area, $sanitize, &$deletes));
1273
1274
	if (!empty($hook_errors) && is_array($hook_errors))
1275
		$errors = array_merge($errors, $hook_errors);
1276
1277
	// Make those changes!
1278
	if ((!empty($changes) || !empty($deletes)) && empty($context['password_auth_failed']) && empty($errors))
1279
	{
1280
		if (!empty($changes))
1281
			$smcFunc['db_insert']('replace',
1282
				'{db_prefix}themes',
1283
				array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
1284
				$changes,
1285
				array('id_theme', 'variable', 'id_member')
1286
			);
1287
		if (!empty($deletes))
1288
			$smcFunc['db_query']('','
1289
				DELETE FROM {db_prefix}themes
1290
				WHERE id_theme = {int:id_theme} AND
1291
						variable = {string:variable} AND
1292
						id_member = {int:id_member}',
1293
				$deletes
1294
				);
1295
		if (!empty($log_changes) && !empty($modSettings['modlog_enabled']))
1296
		{
1297
			require_once($sourcedir . '/Logging.php');
1298
			logActions($log_changes);
1299
		}
1300
	}
1301
1302
	if ($returnErrors)
1303
		return $errors;
1304
}
1305
1306
/**
1307
 * Show all the users buddies, as well as a add/delete interface.
1308
 *
1309
 * @param int $memID The ID of the member
1310
 */
1311
function editBuddyIgnoreLists($memID)
1312
{
1313
	global $context, $txt, $modSettings;
1314
1315
	// Do a quick check to ensure people aren't getting here illegally!
1316
	if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
1317
		fatal_lang_error('no_access', false);
1318
1319
	// Can we email the user direct?
1320
	$context['can_moderate_forum'] = allowedTo('moderate_forum');
1321
	$context['can_send_email'] = allowedTo('moderate_forum');
1322
1323
	$subActions = array(
1324
		'buddies' => array('editBuddies', $txt['editBuddies']),
1325
		'ignore' => array('editIgnoreList', $txt['editIgnoreList']),
1326
	);
1327
1328
	$context['list_area'] = isset($_GET['sa']) && isset($subActions[$_GET['sa']]) ? $_GET['sa'] : 'buddies';
1329
1330
	// Create the tabs for the template.
1331
	$context[$context['profile_menu_name']]['tab_data'] = array(
1332
		'title' => $txt['editBuddyIgnoreLists'],
1333
		'description' => $txt['buddy_ignore_desc'],
1334
		'icon' => 'profile_hd.png',
1335
		'tabs' => array(
1336
			'buddies' => array(),
1337
			'ignore' => array(),
1338
		),
1339
	);
1340
1341
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
1342
1343
	// Pass on to the actual function.
1344
	$context['sub_template'] = $subActions[$context['list_area']][0];
1345
	$call = call_helper($subActions[$context['list_area']][0], true);
1346
1347
	if (!empty($call))
1348
		call_user_func($call, $memID);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $function of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

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

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2224
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2225
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
2226
 */
2227
function alert_delete($toDelete, $memID = false)
2228
{
2229
	global $smcFunc;
2230
2231
	if (empty($toDelete))
2232
		return false;
2233
2234
	$toDelete = (array) $toDelete;
2235
2236
	$smcFunc['db_query']('', '
2237
		DELETE FROM {db_prefix}user_alerts
2238
		WHERE id_alert IN({array_int:toDelete})',
2239
		array(
2240
			'toDelete' => $toDelete,
2241
		)
2242
	);
2243
2244
	// Gotta know how many unread alerts are left.
2245
	if ($memID)
2246
	{
2247
		$count = alert_count($memID, true);
0 ignored issues
show
Bug introduced by
It seems like $memID can also be of type true; however, parameter $memID of alert_count() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2247
		$count = alert_count(/** @scrutinizer ignore-type */ $memID, true);
Loading history...
2248
2249
		updateMemberData($memID, array('alerts' => $count));
2250
2251
		// Might want to know this.
2252
		return $count;
2253
	}
2254
}
2255
2256
/**
2257
 * Counts how many alerts a user has - either unread or all depending on $unread
2258
 * We can't use db_num_rows here, as we have to determine what boards the user can see
2259
 * Possibly in future versions as database support for json is mainstream, we can simplify this.
2260
 *
2261
 * @param int $memID The user ID.
2262
 * @param bool $unread Whether to only count unread alerts.
2263
 * @return int The number of requested alerts
2264
 */
2265
function alert_count($memID, $unread = false)
2266
{
2267
	global $smcFunc, $user_info;
2268
2269
	if (empty($memID))
2270
		return false;
2271
2272
	// We have to do this the slow way as to iterate over all possible boards the user can see.
2273
	$request = $smcFunc['db_query']('', '
2274
		SELECT id_alert, extra
2275
		FROM {db_prefix}user_alerts
2276
		WHERE id_member = {int:id_member}
2277
			'.($unread ? '
2278
			AND is_read = 0' : ''),
2279
		array(
2280
			'id_member' => $memID,
2281
		)
2282
	);
2283
2284
	// First we dump alerts and possible boards information out.
2285
	$alerts = array();
2286
	$boards = array();
2287
	$possible_boards = array();
2288
	while ($row = $smcFunc['db_fetch_assoc']($request))
2289
	{
2290
		$alerts[$row['id_alert']] = !empty($row['extra']) ? $smcFunc['json_decode']($row['extra'], true) : array();
2291
2292
		// Only add to possible boards ones that are not empty and that we haven't set before.
2293
		if (!empty($alerts[$row['id_alert']]['board']) && !isset($possible_boards[$alerts[$row['id_alert']]['board']]))
2294
			$possible_boards[$alerts[$row['id_alert']]['board']] = $alerts[$row['id_alert']]['board'];
2295
	}
2296
	$smcFunc['db_free_result']($request);
2297
2298
	// If this isn't the current user, get their boards.
2299
	if (isset($user_info) && $user_info['id'] != $memID)
2300
	{
2301
		$query_see_board = build_query_board($memID);
2302
		$query_see_board = $query_see_board['query_see_board'];
2303
	}
2304
2305
	// Find only the boards they can see.
2306
	if (!empty($possible_boards))
2307
	{
2308
		$request = $smcFunc['db_query']('', '
2309
			SELECT id_board
2310
			FROM {db_prefix}boards AS b
2311
			WHERE ' . (!empty($query_see_board) ? '{raw:query_see_board}' : '{query_see_board}') . '
2312
				AND id_board IN ({array_int:boards})',
2313
			array(
2314
				'boards' => array_keys($possible_boards),
2315
				'query_see_board' => $query_see_board
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query_see_board does not seem to be defined for all execution paths leading up to this point.
Loading history...
2316
			)
2317
		);
2318
		while ($row = $smcFunc['db_fetch_assoc']($request))
2319
			$boards[$row['id_board']] = $row['id_board'];
2320
	}
2321
	unset($possible_boards);
2322
2323
	// Now check alerts again and remove any they can't see.
2324
	foreach ($alerts as $id_alert => $extra)
2325
		if (!isset($boards[$extra['board']]))
2326
			unset($alerts[$id_alert]);		
2327
2328
	return count($alerts);
2329
}
2330
2331
/**
2332
 * Handles alerts related to topics and posts
2333
 *
2334
 * @param int $memID The ID of the member
2335
 */
2336
function alert_notifications_topics($memID)
2337
{
2338
	global $txt, $scripturl, $context, $modSettings, $sourcedir;
2339
2340
	// Because of the way this stuff works, we want to do this ourselves.
2341
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
2342
	{
2343
		checkSession();
2344
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2345
2346
		makeNotificationChanges($memID);
2347
		$context['profile_updated'] = $txt['profile_updated_own'];
2348
	}
2349
2350
	// Now set up for the token check.
2351
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2352
	createToken($context['token_check'], 'post');
2353
2354
	// Gonna want this for the list.
2355
	require_once($sourcedir . '/Subs-List.php');
2356
2357
	// Do the topic notifications.
2358
	$listOptions = array(
2359
		'id' => 'topic_notification_list',
2360
		'width' => '100%',
2361
		'items_per_page' => $modSettings['defaultMaxListItems'],
2362
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2363
		'no_items_align' => 'left',
2364
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2365
		'default_sort_col' => 'last_post',
2366
		'get_items' => array(
2367
			'function' => 'list_getTopicNotifications',
2368
			'params' => array(
2369
				$memID,
2370
			),
2371
		),
2372
		'get_count' => array(
2373
			'function' => 'list_getTopicNotificationCount',
2374
			'params' => array(
2375
				$memID,
2376
			),
2377
		),
2378
		'columns' => array(
2379
			'subject' => array(
2380
				'header' => array(
2381
					'value' => $txt['notifications_topics'],
2382
					'class' => 'lefttext',
2383
				),
2384
				'data' => array(
2385
					'function' => function($topic) use ($txt)
2386
					{
2387
						$link = $topic['link'];
2388
2389
						if ($topic['new'])
2390
							$link .= ' <a href="' . $topic['new_href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2391
2392
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2393
2394
						return $link;
2395
					},
2396
				),
2397
				'sort' => array(
2398
					'default' => 'ms.subject',
2399
					'reverse' => 'ms.subject DESC',
2400
				),
2401
			),
2402
			'started_by' => array(
2403
				'header' => array(
2404
					'value' => $txt['started_by'],
2405
					'class' => 'lefttext',
2406
				),
2407
				'data' => array(
2408
					'db' => 'poster_link',
2409
				),
2410
				'sort' => array(
2411
					'default' => 'real_name_col',
2412
					'reverse' => 'real_name_col DESC',
2413
				),
2414
			),
2415
			'last_post' => array(
2416
				'header' => array(
2417
					'value' => $txt['last_post'],
2418
					'class' => 'lefttext',
2419
				),
2420
				'data' => array(
2421
					'sprintf' => array(
2422
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2423
						'params' => array(
2424
							'updated' => false,
2425
							'poster_updated_link' => false,
2426
						),
2427
					),
2428
				),
2429
				'sort' => array(
2430
					'default' => 'ml.id_msg DESC',
2431
					'reverse' => 'ml.id_msg',
2432
				),
2433
			),
2434
			'alert' => array(
2435
				'header' => array(
2436
					'value' => $txt['notify_what_how'],
2437
					'class' => 'lefttext',
2438
				),
2439
				'data' => array(
2440
					'function' => function($topic) use ($txt)
2441
					{
2442
						$pref = $topic['notify_pref'];
2443
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2444
						return $txt['notify_topic_' . $mode];
2445
					},
2446
				),
2447
			),
2448
			'delete' => array(
2449
				'header' => array(
2450
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2451
					'style' => 'width: 4%;',
2452
					'class' => 'centercol',
2453
				),
2454
				'data' => array(
2455
					'sprintf' => array(
2456
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d">',
2457
						'params' => array(
2458
							'id' => false,
2459
						),
2460
					),
2461
					'class' => 'centercol',
2462
				),
2463
			),
2464
		),
2465
		'form' => array(
2466
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2467
			'include_sort' => true,
2468
			'include_start' => true,
2469
			'hidden_fields' => array(
2470
				'u' => $memID,
2471
				'sa' => $context['menu_item_selected'],
2472
				$context['session_var'] => $context['session_id'],
2473
			),
2474
			'token' => $context['token_check'],
2475
		),
2476
		'additional_rows' => array(
2477
			array(
2478
				'position' => 'bottom_of_list',
2479
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button" />
2480
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2481
				'class' => 'floatright',
2482
			),
2483
		),
2484
	);
2485
2486
	// Create the notification list.
2487
	createList($listOptions);
2488
}
2489
2490
/**
2491
 * Handles preferences related to board-level notifications
2492
 *
2493
 * @param int $memID The ID of the member
2494
 */
2495
function alert_notifications_boards($memID)
2496
{
2497
	global $txt, $scripturl, $context, $sourcedir;
2498
2499
	// Because of the way this stuff works, we want to do this ourselves.
2500
	if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $_POSt seems to never exist and therefore isset should always be false.
Loading history...
2501
	{
2502
		checkSession();
2503
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2504
2505
		makeNotificationChanges($memID);
2506
		$context['profile_updated'] = $txt['profile_updated_own'];
2507
	}
2508
2509
	// Now set up for the token check.
2510
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2511
	createToken($context['token_check'], 'post');
2512
2513
	// Gonna want this for the list.
2514
	require_once($sourcedir . '/Subs-List.php');
2515
2516
	// Fine, start with the board list.
2517
	$listOptions = array(
2518
		'id' => 'board_notification_list',
2519
		'width' => '100%',
2520
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2521
		'no_items_align' => 'left',
2522
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2523
		'default_sort_col' => 'board_name',
2524
		'get_items' => array(
2525
			'function' => 'list_getBoardNotifications',
2526
			'params' => array(
2527
				$memID,
2528
			),
2529
		),
2530
		'columns' => array(
2531
			'board_name' => array(
2532
				'header' => array(
2533
					'value' => $txt['notifications_boards'],
2534
					'class' => 'lefttext',
2535
				),
2536
				'data' => array(
2537
					'function' => function($board) use ($txt)
2538
					{
2539
						$link = $board['link'];
2540
2541
						if ($board['new'])
2542
							$link .= ' <a href="' . $board['href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2543
2544
						return $link;
2545
					},
2546
				),
2547
				'sort' => array(
2548
					'default' => 'name',
2549
					'reverse' => 'name DESC',
2550
				),
2551
			),
2552
			'alert' => array(
2553
				'header' => array(
2554
					'value' => $txt['notify_what_how'],
2555
					'class' => 'lefttext',
2556
				),
2557
				'data' => array(
2558
					'function' => function($board) use ($txt)
2559
					{
2560
						$pref = $board['notify_pref'];
2561
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2562
						return $txt['notify_board_' . $mode];
2563
					},
2564
				),
2565
			),
2566
			'delete' => array(
2567
				'header' => array(
2568
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2569
					'style' => 'width: 4%;',
2570
					'class' => 'centercol',
2571
				),
2572
				'data' => array(
2573
					'sprintf' => array(
2574
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d">',
2575
						'params' => array(
2576
							'id' => false,
2577
						),
2578
					),
2579
					'class' => 'centercol',
2580
				),
2581
			),
2582
		),
2583
		'form' => array(
2584
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2585
			'include_sort' => true,
2586
			'include_start' => true,
2587
			'hidden_fields' => array(
2588
				'u' => $memID,
2589
				'sa' => $context['menu_item_selected'],
2590
				$context['session_var'] => $context['session_id'],
2591
			),
2592
			'token' => $context['token_check'],
2593
		),
2594
		'additional_rows' => array(
2595
			array(
2596
				'position' => 'bottom_of_list',
2597
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button">
2598
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2599
				'class' => 'floatright',
2600
			),
2601
		),
2602
	);
2603
2604
	// Create the board notification list.
2605
	createList($listOptions);
2606
}
2607
2608
/**
2609
 * Determins how many topics a user has requested notifications for
2610
 *
2611
 * @param int $memID The ID of the member
2612
 * @return int The number of topic notifications for this user
2613
 */
2614
function list_getTopicNotificationCount($memID)
2615
{
2616
	global $smcFunc, $user_info, $modSettings;
2617
2618
	$request = $smcFunc['db_query']('', '
2619
		SELECT COUNT(*)
2620
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2621
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2622
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2623
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2624
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2625
			AND t.approved = {int:is_approved}' : ''),
2626
		array(
2627
			'selected_member' => $memID,
2628
			'is_approved' => 1,
2629
		)
2630
	);
2631
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2632
	$smcFunc['db_free_result']($request);
2633
2634
	return (int) $totalNotifications;
2635
}
2636
2637
/**
2638
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2639
 *
2640
 * @param int $start Which item to start with (for pagination purposes)
2641
 * @param int $items_per_page How many items to display on each page
2642
 * @param string $sort A string indicating how to sort the results
2643
 * @param int $memID The ID of the member
2644
 * @return array An array of information about the topics a user has subscribed to
2645
 */
2646
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2647
{
2648
	global $smcFunc, $scripturl, $user_info, $modSettings, $sourcedir;
2649
2650
	require_once($sourcedir . '/Subs-Notify.php');
2651
	$prefs = getNotifyPrefs($memID);
2652
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2653
2654
	// All the topics with notification on...
2655
	$request = $smcFunc['db_query']('', '
2656
		SELECT
2657
			COALESCE(lt.id_msg, COALESCE(lmr.id_msg, -1)) + 1 AS new_from, b.id_board, b.name,
2658
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2659
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2660
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2661
			lt.unwatched
2662
		FROM {db_prefix}log_notify AS ln
2663
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2664
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2665
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2666
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2667
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2668
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2669
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2670
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2671
		WHERE ln.id_member = {int:selected_member}
2672
		ORDER BY {raw:sort}
2673
		LIMIT {int:offset}, {int:items_per_page}',
2674
		array(
2675
			'current_member' => $user_info['id'],
2676
			'is_approved' => 1,
2677
			'selected_member' => $memID,
2678
			'sort' => $sort,
2679
			'offset' => $start,
2680
			'items_per_page' => $items_per_page,
2681
		)
2682
	);
2683
	$notification_topics = array();
2684
	while ($row = $smcFunc['db_fetch_assoc']($request))
2685
	{
2686
		censorText($row['subject']);
2687
2688
		$notification_topics[] = array(
2689
			'id' => $row['id_topic'],
2690
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2691
			'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>',
2692
			'subject' => $row['subject'],
2693
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2694
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2695
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2696
			'new_from' => $row['new_from'],
2697
			'updated' => timeformat($row['poster_time']),
2698
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2699
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2700
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2701
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2702
			'unwatched' => $row['unwatched'],
2703
		);
2704
	}
2705
	$smcFunc['db_free_result']($request);
2706
2707
	return $notification_topics;
2708
}
2709
2710
/**
2711
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2712
 *
2713
 * @param int $start Which item to start with (not used here)
2714
 * @param int $items_per_page How many items to show on each page (not used here)
2715
 * @param string $sort A string indicating how to sort the results
2716
 * @param int $memID The ID of the member
2717
 * @return array An array of information about all the boards a user is subscribed to
2718
 */
2719
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2720
{
2721
	global $smcFunc, $scripturl, $user_info, $sourcedir;
2722
2723
	require_once($sourcedir . '/Subs-Notify.php');
2724
	$prefs = getNotifyPrefs($memID);
2725
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2726
2727
	$request = $smcFunc['db_query']('', '
2728
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2729
		FROM {db_prefix}log_notify AS ln
2730
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2731
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2732
		WHERE ln.id_member = {int:selected_member}
2733
			AND {query_see_board}
2734
		ORDER BY {raw:sort}',
2735
		array(
2736
			'current_member' => $user_info['id'],
2737
			'selected_member' => $memID,
2738
			'sort' => $sort,
2739
		)
2740
	);
2741
	$notification_boards = array();
2742
	while ($row = $smcFunc['db_fetch_assoc']($request))
2743
		$notification_boards[] = array(
2744
			'id' => $row['id_board'],
2745
			'name' => $row['name'],
2746
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2747
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2748
			'new' => $row['board_read'] < $row['id_msg_updated'],
2749
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2750
		);
2751
	$smcFunc['db_free_result']($request);
2752
2753
	return $notification_boards;
2754
}
2755
2756
/**
2757
 * Loads the theme options for a user
2758
 *
2759
 * @param int $memID The ID of the member
2760
 */
2761
function loadThemeOptions($memID)
2762
{
2763
	global $context, $options, $cur_profile, $smcFunc;
2764
2765
	if (isset($_POST['default_options']))
2766
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2767
2768
	if ($context['user']['is_owner'])
2769
	{
2770
		$context['member']['options'] = $options;
2771
		if (isset($_POST['options']) && is_array($_POST['options']))
2772
			foreach ($_POST['options'] as $k => $v)
2773
				$context['member']['options'][$k] = $v;
2774
	}
2775
	else
2776
	{
2777
		$request = $smcFunc['db_query']('', '
2778
			SELECT id_member, variable, value
2779
			FROM {db_prefix}themes
2780
			WHERE id_theme IN (1, {int:member_theme})
2781
				AND id_member IN (-1, {int:selected_member})',
2782
			array(
2783
				'member_theme' => (int) $cur_profile['id_theme'],
2784
				'selected_member' => $memID,
2785
			)
2786
		);
2787
		$temp = array();
2788
		while ($row = $smcFunc['db_fetch_assoc']($request))
2789
		{
2790
			if ($row['id_member'] == -1)
2791
			{
2792
				$temp[$row['variable']] = $row['value'];
2793
				continue;
2794
			}
2795
2796
			if (isset($_POST['options'][$row['variable']]))
2797
				$row['value'] = $_POST['options'][$row['variable']];
2798
			$context['member']['options'][$row['variable']] = $row['value'];
2799
		}
2800
		$smcFunc['db_free_result']($request);
2801
2802
		// Load up the default theme options for any missing.
2803
		foreach ($temp as $k => $v)
2804
		{
2805
			if (!isset($context['member']['options'][$k]))
2806
				$context['member']['options'][$k] = $v;
2807
		}
2808
	}
2809
}
2810
2811
/**
2812
 * Handles the "ignored boards" section of the profile (if enabled)
2813
 *
2814
 * @param int $memID The ID of the member
2815
 */
2816
function ignoreboards($memID)
2817
{
2818
	global $context, $modSettings, $smcFunc, $cur_profile, $sourcedir;
2819
2820
	// Have the admins enabled this option?
2821
	if (empty($modSettings['allow_ignore_boards']))
2822
		fatal_lang_error('ignoreboards_disallowed', 'user');
2823
2824
	// Find all the boards this user is allowed to see.
2825
	$request = $smcFunc['db_query']('order_by_board_order', '
2826
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
2827
			'. (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
2828
		FROM {db_prefix}boards AS b
2829
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2830
		WHERE {query_see_board}
2831
			AND redirect = {string:empty_string}',
2832
		array(
2833
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
2834
			'empty_string' => '',
2835
		)
2836
	);
2837
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
2838
	$context['categories'] = array();
2839
	while ($row = $smcFunc['db_fetch_assoc']($request))
2840
	{
2841
		// This category hasn't been set up yet..
2842
		if (!isset($context['categories'][$row['id_cat']]))
2843
			$context['categories'][$row['id_cat']] = array(
2844
				'id' => $row['id_cat'],
2845
				'name' => $row['cat_name'],
2846
				'boards' => array()
2847
			);
2848
2849
		// Set this board up, and let the template know when it's a child.  (indent them..)
2850
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
2851
			'id' => $row['id_board'],
2852
			'name' => $row['name'],
2853
			'child_level' => $row['child_level'],
2854
			'selected' => $row['is_ignored'],
2855
		);
2856
	}
2857
	$smcFunc['db_free_result']($request);
2858
2859
	require_once($sourcedir . '/Subs-Boards.php');
2860
	sortCategories($context['categories']);
2861
2862
	// Now, let's sort the list of categories into the boards for templates that like that.
2863
	$temp_boards = array();
2864
	foreach ($context['categories'] as $category)
2865
	{
2866
		// Include a list of boards per category for easy toggling.
2867
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
2868
2869
		$temp_boards[] = array(
2870
			'name' => $category['name'],
2871
			'child_ids' => array_keys($category['boards'])
2872
		);
2873
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
2874
	}
2875
2876
	$max_boards = ceil(count($temp_boards) / 2);
2877
	if ($max_boards == 1)
2878
		$max_boards = 2;
2879
2880
	// Now, alternate them so they can be shown left and right ;).
2881
	$context['board_columns'] = array();
2882
	for ($i = 0; $i < $max_boards; $i++)
2883
	{
2884
		$context['board_columns'][] = $temp_boards[$i];
2885
		if (isset($temp_boards[$i + $max_boards]))
2886
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
2887
		else
2888
			$context['board_columns'][] = array();
2889
	}
2890
2891
	loadThemeOptions($memID);
2892
}
2893
2894
/**
2895
 * Load all the languages for the profile
2896
 * .
2897
 * @return bool Whether or not the forum has multiple languages installed
2898
 */
2899
function profileLoadLanguages()
2900
{
2901
	global $context;
2902
2903
	$context['profile_languages'] = array();
2904
2905
	// Get our languages!
2906
	getLanguages();
2907
2908
	// Setup our languages.
2909
	foreach ($context['languages'] as $lang)
2910
	{
2911
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
2912
	}
2913
	ksort($context['profile_languages']);
2914
2915
	// Return whether we should proceed with this.
2916
	return count($context['profile_languages']) > 1 ? true : false;
2917
}
2918
2919
/**
2920
 * Handles the "manage groups" section of the profile
2921
 *
2922
 * @return true Always returns true
2923
 */
2924
function profileLoadGroups()
2925
{
2926
	global $cur_profile, $txt, $context, $smcFunc, $user_settings;
2927
2928
	$context['member_groups'] = array(
2929
		0 => array(
2930
			'id' => 0,
2931
			'name' => $txt['no_primary_membergroup'],
2932
			'is_primary' => $cur_profile['id_group'] == 0,
2933
			'can_be_additional' => false,
2934
			'can_be_primary' => true,
2935
		)
2936
	);
2937
	$curGroups = explode(',', $cur_profile['additional_groups']);
2938
2939
	// Load membergroups, but only those groups the user can assign.
2940
	$request = $smcFunc['db_query']('', '
2941
		SELECT group_name, id_group, hidden
2942
		FROM {db_prefix}membergroups
2943
		WHERE id_group != {int:moderator_group}
2944
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
2945
			AND group_type != {int:is_protected}') . '
2946
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
2947
		array(
2948
			'moderator_group' => 3,
2949
			'min_posts' => -1,
2950
			'is_protected' => 1,
2951
			'newbie_group' => 4,
2952
		)
2953
	);
2954
	while ($row = $smcFunc['db_fetch_assoc']($request))
2955
	{
2956
		// We should skip the administrator group if they don't have the admin_forum permission!
2957
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
2958
			continue;
2959
2960
		$context['member_groups'][$row['id_group']] = array(
2961
			'id' => $row['id_group'],
2962
			'name' => $row['group_name'],
2963
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
2964
			'is_additional' => in_array($row['id_group'], $curGroups),
2965
			'can_be_additional' => true,
2966
			'can_be_primary' => $row['hidden'] != 2,
2967
		);
2968
	}
2969
	$smcFunc['db_free_result']($request);
2970
2971
	$context['member']['group_id'] = $user_settings['id_group'];
2972
2973
	return true;
2974
}
2975
2976
/**
2977
 * Load key signature context data.
2978
 *
2979
 * @return true Always returns true
2980
 */
2981
function profileLoadSignatureData()
2982
{
2983
	global $modSettings, $context, $txt, $cur_profile, $memberContext;
2984
2985
	// Signature limits.
2986
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
2987
	$sig_limits = explode(',', $sig_limits);
2988
2989
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
2990
	$context['signature_limits'] = array(
2991
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
2992
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
2993
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
2994
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
2995
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
2996
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
2997
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
2998
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
2999
	);
3000
	// Kept this line in for backwards compatibility!
3001
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
3002
	// Warning message for signature image limits?
3003
	$context['signature_warning'] = '';
3004
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
3005
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
3006
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
3007
		$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']);
3008
3009
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_character_set'] == 'UTF-8' || function_exists('iconv'))));
3010
3011
	if (empty($context['do_preview']))
3012
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
3013
	else
3014
	{
3015
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
3016
		$validation = profileValidateSignature($signature);
3017
		if (empty($context['post_errors']))
3018
		{
3019
			loadLanguage('Errors');
3020
			$context['post_errors'] = array();
3021
		}
3022
		$context['post_errors'][] = 'signature_not_yet_saved';
3023
		if ($validation !== true && $validation !== false)
3024
			$context['post_errors'][] = $validation;
3025
3026
		censorText($context['member']['signature']);
3027
		$context['member']['current_signature'] = $context['member']['signature'];
3028
		censorText($signature);
3029
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']]);
3030
		$context['member']['signature'] = $_POST['signature'];
3031
	}
3032
3033
	// Load the spell checker?
3034
	if ($context['show_spellchecking'])
3035
		loadJavaScriptFile('spellcheck.js', array('defer' => false, 'minimize' => true), 'smf_spellcheck');
3036
3037
	return true;
3038
}
3039
3040
/**
3041
 * Load avatar context data.
3042
 *
3043
 * @return true Always returns true
3044
 */
3045
function profileLoadAvatarData()
3046
{
3047
	global $context, $cur_profile, $modSettings, $scripturl;
3048
3049
	$context['avatar_url'] = $modSettings['avatar_url'];
3050
3051
	// Default context.
3052
	$context['member']['avatar'] += array(
3053
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
3054
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
3055
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3056
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3057
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3058
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']) || !empty($modSettings['gravatarOverride']),
3059
	);
3060
3061
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
3062
	{
3063
		$context['member']['avatar'] += array(
3064
			'choice' => 'gravatar',
3065
			'server_pic' => 'blank.png',
3066
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || !empty($modSettings['gravatarOverride']) ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
3067
		);
3068
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
3069
	}
3070
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
3071
	{
3072
		$context['member']['avatar'] += array(
3073
			'choice' => 'upload',
3074
			'server_pic' => 'blank.png',
3075
			'external' => 'http://'
3076
		);
3077
		$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'];
3078
	}
3079
	// Use "avatar_original" here so we show what the user entered even if the image proxy is enabled
3080
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
3081
		$context['member']['avatar'] += array(
3082
			'choice' => 'external',
3083
			'server_pic' => 'blank.png',
3084
			'external' => $cur_profile['avatar_original']
3085
		);
3086
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
3087
		$context['member']['avatar'] += array(
3088
			'choice' => 'server_stored',
3089
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3090
			'external' => 'http://'
3091
		);
3092
	else
3093
		$context['member']['avatar'] += array(
3094
			'choice' => 'none',
3095
			'server_pic' => 'blank.png',
3096
			'external' => 'http://'
3097
		);
3098
3099
	// Get a list of all the avatars.
3100
	if ($context['member']['avatar']['allow_server_stored'])
3101
	{
3102
		$context['avatar_list'] = array();
3103
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3104
	}
3105
	else
3106
		$context['avatars'] = array();
3107
3108
	// Second level selected avatar...
3109
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3110
	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']);
3111
}
3112
3113
/**
3114
 * Save a members group.
3115
 *
3116
 * @param int &$value The ID of the (new) primary group
3117
 * @return true Always returns true
3118
 */
3119
function profileSaveGroups(&$value)
3120
{
3121
	global $profile_vars, $old_profile, $context, $smcFunc, $cur_profile;
3122
3123
	// Do we need to protect some groups?
3124
	if (!allowedTo('admin_forum'))
3125
	{
3126
		$request = $smcFunc['db_query']('', '
3127
			SELECT id_group
3128
			FROM {db_prefix}membergroups
3129
			WHERE group_type = {int:is_protected}',
3130
			array(
3131
				'is_protected' => 1,
3132
			)
3133
		);
3134
		$protected_groups = array(1);
3135
		while ($row = $smcFunc['db_fetch_assoc']($request))
3136
			$protected_groups[] = $row['id_group'];
3137
		$smcFunc['db_free_result']($request);
3138
3139
		$protected_groups = array_unique($protected_groups);
3140
	}
3141
3142
	// The account page allows the change of your id_group - but not to a protected group!
3143
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3144
		$value = (int) $value;
3145
	// ... otherwise it's the old group sir.
3146
	else
3147
		$value = $old_profile['id_group'];
3148
3149
	// Find the additional membergroups (if any)
3150
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3151
	{
3152
		$additional_groups = array();
3153
		foreach ($_POST['additional_groups'] as $group_id)
3154
		{
3155
			$group_id = (int) $group_id;
3156
			if (!empty($group_id) && (empty($protected_groups) || !in_array($group_id, $protected_groups)))
3157
				$additional_groups[] = $group_id;
3158
		}
3159
3160
		// Put the protected groups back in there if you don't have permission to take them away.
3161
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3162
		foreach ($old_additional_groups as $group_id)
3163
		{
3164
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3165
				$additional_groups[] = $group_id;
3166
		}
3167
3168
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3169
		{
3170
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3171
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3172
		}
3173
	}
3174
3175
	// Too often, people remove delete their own account, or something.
3176
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3177
	{
3178
		$stillAdmin = $value == 1 || (isset($additional_groups) && in_array(1, $additional_groups));
3179
3180
		// If they would no longer be an admin, look for any other...
3181
		if (!$stillAdmin)
3182
		{
3183
			$request = $smcFunc['db_query']('', '
3184
				SELECT id_member
3185
				FROM {db_prefix}members
3186
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3187
					AND id_member != {int:selected_member}
3188
				LIMIT 1',
3189
				array(
3190
					'admin_group' => 1,
3191
					'selected_member' => $context['id_member'],
3192
				)
3193
			);
3194
			list ($another) = $smcFunc['db_fetch_row']($request);
3195
			$smcFunc['db_free_result']($request);
3196
3197
			if (empty($another))
3198
				fatal_lang_error('at_least_one_admin', 'critical');
3199
		}
3200
	}
3201
3202
	// If we are changing group status, update permission cache as necessary.
3203
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3204
	{
3205
		if ($context['user']['is_owner'])
3206
			$_SESSION['mc']['time'] = 0;
3207
		else
3208
			updateSettings(array('settings_updated' => time()));
3209
	}
3210
3211
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3212
	call_integration_hook('integrate_profile_profileSaveGroups', array($value, $additional_groups));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $additional_groups does not seem to be defined for all execution paths leading up to this point.
Loading history...
3213
3214
	return true;
3215
}
3216
3217
/**
3218
 * The avatar is incredibly complicated, what with the options... and what not.
3219
 * @todo argh, the avatar here. Take this out of here!
3220
 *
3221
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3222
 * @return bool|string False if success (or if memID is empty and password authentication failed), otherwise a string indicating what error occurred
3223
 */
3224
function profileSaveAvatarData(&$value)
3225
{
3226
	global $modSettings, $sourcedir, $smcFunc, $profile_vars, $cur_profile, $context;
3227
3228
	$memID = $context['id_member'];
3229
	if (empty($memID) && !empty($context['password_auth_failed']))
3230
		return false;
3231
3232
	require_once($sourcedir . '/ManageAttachments.php');
3233
3234
	// We're going to put this on a nice custom dir.
3235
	$uploadDir = $modSettings['custom_avatar_dir'];
3236
	$id_folder = 1;
3237
3238
	$downloadedExternalAvatar = false;
3239
	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']))
3240
	{
3241
		if (!is_writable($uploadDir))
3242
			fatal_lang_error('attachments_no_write', 'critical');
3243
3244
		$url = parse_url($_POST['userpicpersonal']);
3245
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3246
3247
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $attachment_id of getAttachmentFilename(). ( Ignorable by Annotation )

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

3247
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, /** @scrutinizer ignore-type */ false, null, true);
Loading history...
3248
		if ($contents != false && $tmpAvatar = fopen($new_filename, 'wb'))
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $contents of type false|string against false; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
3249
		{
3250
			fwrite($tmpAvatar, $contents);
3251
			fclose($tmpAvatar);
3252
3253
			$downloadedExternalAvatar = true;
3254
			$_FILES['attachment']['tmp_name'] = $new_filename;
3255
		}
3256
	}
3257
3258
	// Removes whatever attachment there was before updating
3259
	if ($value == 'none')
3260
	{
3261
		$profile_vars['avatar'] = '';
3262
3263
		// Reset the attach ID.
3264
		$cur_profile['id_attach'] = 0;
3265
		$cur_profile['attachment_type'] = 0;
3266
		$cur_profile['filename'] = '';
3267
3268
		removeAttachments(array('id_member' => $memID));
3269
	}
3270
3271
	// An avatar from the server-stored galleries.
3272
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3273
	{
3274
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3275
		$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']) : '';
3276
3277
		// Clear current profile...
3278
		$cur_profile['id_attach'] = 0;
3279
		$cur_profile['attachment_type'] = 0;
3280
		$cur_profile['filename'] = '';
3281
3282
		// Get rid of their old avatar. (if uploaded.)
3283
		removeAttachments(array('id_member' => $memID));
3284
	}
3285
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3286
	{
3287
		// 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.
3288
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3289
			$profile_vars['avatar'] = 'gravatar://';
3290
		else
3291
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3292
3293
		// Get rid of their old avatar. (if uploaded.)
3294
		removeAttachments(array('id_member' => $memID));
3295
	}
3296
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3297
	{
3298
		// We need these clean...
3299
		$cur_profile['id_attach'] = 0;
3300
		$cur_profile['attachment_type'] = 0;
3301
		$cur_profile['filename'] = '';
3302
3303
		// Remove any attached avatar...
3304
		removeAttachments(array('id_member' => $memID));
3305
3306
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3307
3308
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3309
			$profile_vars['avatar'] = '';
3310
		// Trying to make us do something we'll regret?
3311
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3312
			return 'bad_avatar_invalid_url';
3313
		// Should we check dimensions?
3314
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3315
		{
3316
			// Now let's validate the avatar.
3317
			$sizes = url_image_size($profile_vars['avatar']);
3318
3319
			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']))))
3320
			{
3321
				// Houston, we have a problem. The avatar is too large!!
3322
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3323
					return 'bad_avatar_too_large';
3324
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3325
				{
3326
					// @todo remove this if appropriate
3327
					require_once($sourcedir . '/Subs-Graphics.php');
3328
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3329
					{
3330
						$profile_vars['avatar'] = '';
3331
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3332
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3333
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3334
					}
3335
					else
3336
						return 'bad_avatar';
3337
				}
3338
			}
3339
		}
3340
	}
3341
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3342
	{
3343
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3344
		{
3345
			// Get the dimensions of the image.
3346
			if (!$downloadedExternalAvatar)
3347
			{
3348
				if (!is_writable($uploadDir))
3349
					fatal_lang_error('attachments_no_write', 'critical');
3350
3351
				$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
3352
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3353
					fatal_lang_error('attach_timeout', 'critical');
3354
3355
				$_FILES['attachment']['tmp_name'] = $new_filename;
3356
			}
3357
3358
			$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3359
3360
			// No size, then it's probably not a valid pic.
3361
			if ($sizes === false)
3362
			{
3363
				@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

3363
				/** @scrutinizer ignore-unhandled */ @unlink($_FILES['attachment']['tmp_name']);

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...
3364
				return 'bad_avatar';
3365
			}
3366
			// Check whether the image is too large.
3367
			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']))
3368
			{
3369
				if (!empty($modSettings['avatar_resize_upload']))
3370
				{
3371
					// Attempt to chmod it.
3372
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3373
3374
					// @todo remove this require when appropriate
3375
					require_once($sourcedir . '/Subs-Graphics.php');
3376
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3377
					{
3378
						@unlink($_FILES['attachment']['tmp_name']);
3379
						return 'bad_avatar';
3380
					}
3381
3382
					// Reset attachment avatar data.
3383
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3384
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3385
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3386
				}
3387
3388
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3389
				else
3390
				{
3391
					@unlink($_FILES['attachment']['tmp_name']);
3392
					return 'bad_avatar_too_large';
3393
				}
3394
			}
3395
3396
			// So far, so good, checks lies ahead!
3397
			elseif (is_array($sizes))
0 ignored issues
show
introduced by
The condition is_array($sizes) is always true.
Loading history...
3398
			{
3399
				// Now try to find an infection.
3400
				require_once($sourcedir . '/Subs-Graphics.php');
3401
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3402
				{
3403
					// It's bad. Try to re-encode the contents?
3404
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3405
					{
3406
						@unlink($_FILES['attachment']['tmp_name']);
3407
						return 'bad_avatar_fail_reencode';
3408
					}
3409
					// We were successful. However, at what price?
3410
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3411
					// Hard to believe this would happen, but can you bet?
3412
					if ($sizes === false)
3413
					{
3414
						@unlink($_FILES['attachment']['tmp_name']);
3415
						return 'bad_avatar';
3416
					}
3417
				}
3418
3419
				$extensions = array(
3420
					'1' => 'gif',
3421
					'2' => 'jpg',
3422
					'3' => 'png',
3423
					'6' => 'bmp'
3424
				);
3425
3426
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3427
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3428
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3429
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3430
				$file_hash = '';
3431
3432
				// Remove previous attachments this member might have had.
3433
				removeAttachments(array('id_member' => $memID));
3434
3435
				$cur_profile['id_attach'] = $smcFunc['db_insert']('',
3436
					'{db_prefix}attachments',
3437
					array(
3438
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3439
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3440
					),
3441
					array(
3442
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3443
						(int) $width, (int) $height, $mime_type, $id_folder,
3444
					),
3445
					array('id_attach'),
3446
					1
3447
				);
3448
3449
				$cur_profile['filename'] = $destName;
3450
				$cur_profile['attachment_type'] = 1;
3451
3452
				$destinationPath = $uploadDir . '/' . (empty($file_hash) ? $destName : $cur_profile['id_attach'] . '_' . $file_hash . '.dat');
0 ignored issues
show
introduced by
The condition empty($file_hash) is always true.
Loading history...
3453
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3454
				{
3455
					// I guess a man can try.
3456
					removeAttachments(array('id_member' => $memID));
3457
					fatal_lang_error('attach_timeout', 'critical');
3458
				}
3459
3460
				// Attempt to chmod it.
3461
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3462
			}
3463
			$profile_vars['avatar'] = '';
3464
3465
			// Delete any temporary file.
3466
			if (file_exists($_FILES['attachment']['tmp_name']))
3467
				@unlink($_FILES['attachment']['tmp_name']);
3468
		}
3469
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3470
		else
3471
			$profile_vars['avatar'] = '';
3472
	}
3473
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3474
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3475
	else
3476
		$profile_vars['avatar'] = '';
3477
3478
	// Setup the profile variables so it shows things right on display!
3479
	$cur_profile['avatar'] = $profile_vars['avatar'];
3480
3481
	return false;
3482
}
3483
3484
/**
3485
 * Validate the signature
3486
 *
3487
 * @param string &$value The new signature
3488
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3489
 */
3490
function profileValidateSignature(&$value)
3491
{
3492
	global $sourcedir, $modSettings, $smcFunc, $txt;
3493
3494
	require_once($sourcedir . '/Subs-Post.php');
3495
3496
	// Admins can do whatever they hell they want!
3497
	if (!allowedTo('admin_forum'))
3498
	{
3499
		// Load all the signature limits.
3500
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3501
		$sig_limits = explode(',', $sig_limits);
3502
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3503
3504
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3505
3506
		// Too many lines?
3507
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3508
		{
3509
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3510
			return 'signature_max_lines';
3511
		}
3512
3513
		// Too many images?!
3514
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
3515
		{
3516
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3517
			return 'signature_max_image_count';
3518
		}
3519
3520
		// What about too many smileys!
3521
		$smiley_parsed = $unparsed_signature;
3522
		parsesmileys($smiley_parsed);
3523
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3524
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3525
			return 'signature_allow_smileys';
3526
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3527
		{
3528
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3529
			return 'signature_max_smileys';
3530
		}
3531
3532
		// Maybe we are abusing font sizes?
3533
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3534
		{
3535
			foreach ($matches[1] as $ind => $size)
3536
			{
3537
				$limit_broke = 0;
3538
				// Attempt to allow all sizes of abuse, so to speak.
3539
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
3540
					$limit_broke = $sig_limits[7] . 'px';
3541
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3542
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3543
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3544
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3545
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3546
					$limit_broke = 'large';
3547
3548
				if ($limit_broke)
3549
				{
3550
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3551
					return 'signature_max_font_size';
3552
				}
3553
			}
3554
		}
3555
3556
		// The difficult one - image sizes! Don't error on this - just fix it.
3557
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3558
		{
3559
			// Get all BBC tags...
3560
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3561
			// ... and all HTML ones.
3562
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?' . '>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3563
			// And stick the HTML in the BBC.
3564
			if (!empty($matches2))
3565
			{
3566
				foreach ($matches2[0] as $ind => $dummy)
3567
				{
3568
					$matches[0][] = $matches2[0][$ind];
3569
					$matches[1][] = '';
3570
					$matches[2][] = '';
3571
					$matches[3][] = '';
3572
					$matches[4][] = '';
3573
					$matches[5][] = '';
3574
					$matches[6][] = '';
3575
					$matches[7][] = $matches2[1][$ind];
3576
				}
3577
			}
3578
3579
			$replaces = array();
3580
			// Try to find all the images!
3581
			if (!empty($matches))
3582
			{
3583
				foreach ($matches[0] as $key => $image)
3584
				{
3585
					$width = -1; $height = -1;
3586
3587
					// Does it have predefined restraints? Width first.
3588
					if ($matches[6][$key])
3589
						$matches[2][$key] = $matches[6][$key];
3590
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
3591
					{
3592
						$width = $sig_limits[5];
3593
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3594
					}
3595
					elseif ($matches[2][$key])
3596
						$width = $matches[2][$key];
3597
					// ... and height.
3598
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
3599
					{
3600
						$height = $sig_limits[6];
3601
						if ($width != -1)
3602
							$width = $width * ($height / $matches[4][$key]);
3603
					}
3604
					elseif ($matches[4][$key])
3605
						$height = $matches[4][$key];
3606
3607
					// If the dimensions are still not fixed - we need to check the actual image.
3608
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
3609
					{
3610
						$sizes = url_image_size($matches[7][$key]);
3611
						if (is_array($sizes))
3612
						{
3613
							// Too wide?
3614
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3615
							{
3616
								$width = $sig_limits[5];
3617
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3618
							}
3619
							// Too high?
3620
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3621
							{
3622
								$height = $sig_limits[6];
3623
								if ($width == -1)
3624
									$width = $sizes[0];
3625
								$width = $width * ($height / $sizes[1]);
3626
							}
3627
							elseif ($width != -1)
3628
								$height = $sizes[1];
3629
						}
3630
					}
3631
3632
					// Did we come up with some changes? If so remake the string.
3633
					if ($width != -1 || $height != -1)
3634
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3635
				}
3636
				if (!empty($replaces))
3637
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3638
			}
3639
		}
3640
3641
		// Any disabled BBC?
3642
		$disabledSigBBC = implode('|', $disabledTags);
3643
		if (!empty($disabledSigBBC))
3644
		{
3645
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3646
			{
3647
				$disabledTags = array_unique($disabledTags);
3648
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3649
				return 'signature_disabled_bbc';
3650
			}
3651
		}
3652
	}
3653
3654
	preparsecode($value);
3655
3656
	// Too long?
3657
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3658
	{
3659
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3660
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3661
		return 'signature_max_length';
3662
	}
3663
3664
	return true;
3665
}
3666
3667
/**
3668
 * Validate an email address.
3669
 *
3670
 * @param string $email The email address to validate
3671
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3672
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3673
 */
3674
function profileValidateEmail($email, $memID = 0)
3675
{
3676
	global $smcFunc;
3677
3678
	$email = strtr($email, array('&#039;' => '\''));
3679
3680
	// Check the name and email for validity.
3681
	if (trim($email) == '')
3682
		return 'no_email';
3683
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3684
		return 'bad_email';
3685
3686
	// Email addresses should be and stay unique.
3687
	$request = $smcFunc['db_query']('', '
3688
		SELECT id_member
3689
		FROM {db_prefix}members
3690
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3691
			email_address = {string:email_address}
3692
		LIMIT 1',
3693
		array(
3694
			'selected_member' => $memID,
3695
			'email_address' => $email,
3696
		)
3697
	);
3698
3699
	if ($smcFunc['db_num_rows']($request) > 0)
3700
		return 'email_taken';
3701
	$smcFunc['db_free_result']($request);
3702
3703
	return true;
3704
}
3705
3706
/**
3707
 * Reload a user's settings.
3708
 */
3709
function profileReloadUser()
3710
{
3711
	global $modSettings, $context, $cur_profile;
3712
3713
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3714
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3715
3716
	loadUserSettings();
3717
	writeLog();
3718
}
3719
3720
/**
3721
 * Send the user a new activation email if they need to reactivate!
3722
 */
3723
function profileSendActivation()
3724
{
3725
	global $sourcedir, $profile_vars, $context, $scripturl, $smcFunc, $cookiename, $cur_profile, $language, $modSettings;
3726
3727
	require_once($sourcedir . '/Subs-Post.php');
3728
3729
	// Shouldn't happen but just in case.
3730
	if (empty($profile_vars['email_address']))
3731
		return;
3732
3733
	$replacements = array(
3734
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3735
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3736
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3737
	);
3738
3739
	// Send off the email.
3740
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3741
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3742
3743
	// Log the user out.
3744
	$smcFunc['db_query']('', '
3745
		DELETE FROM {db_prefix}log_online
3746
		WHERE id_member = {int:selected_member}',
3747
		array(
3748
			'selected_member' => $context['id_member'],
3749
		)
3750
	);
3751
	$_SESSION['log_time'] = 0;
3752
	$_SESSION['login_' . $cookiename] = $smcFunc['json_encode'](array(0, '', 0));
3753
3754
	if (isset($_COOKIE[$cookiename]))
3755
		$_COOKIE[$cookiename] = '';
3756
3757
	loadUserSettings();
3758
3759
	$context['user']['is_logged'] = false;
3760
	$context['user']['is_guest'] = true;
3761
3762
	redirectexit('action=sendactivation');
3763
}
3764
3765
/**
3766
 * Function to allow the user to choose group membership etc...
3767
 *
3768
 * @param int $memID The ID of the member
3769
 */
3770
function groupMembership($memID)
3771
{
3772
	global $txt, $user_profile, $context, $smcFunc;
3773
3774
	$curMember = $user_profile[$memID];
3775
	$context['primary_group'] = $curMember['id_group'];
3776
3777
	// Can they manage groups?
3778
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3779
	$context['can_manage_protected'] = allowedTo('admin_forum');
3780
	$context['can_edit_primary'] = $context['can_manage_protected'];
3781
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
3782
3783
	// Get all the groups this user is a member of.
3784
	$groups = explode(',', $curMember['additional_groups']);
3785
	$groups[] = $curMember['id_group'];
3786
3787
	// Ensure the query doesn't croak!
3788
	if (empty($groups))
3789
		$groups = array(0);
3790
	// Just to be sure...
3791
	foreach ($groups as $k => $v)
3792
		$groups[$k] = (int) $v;
3793
3794
	// Get all the membergroups they can join.
3795
	$request = $smcFunc['db_query']('', '
3796
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
3797
			COALESCE(lgr.id_member, 0) AS pending
3798
		FROM {db_prefix}membergroups AS mg
3799
			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})
3800
		WHERE (mg.id_group IN ({array_int:group_list})
3801
			OR mg.group_type > {int:nonjoin_group_id})
3802
			AND mg.min_posts = {int:min_posts}
3803
			AND mg.id_group != {int:moderator_group}
3804
		ORDER BY group_name',
3805
		array(
3806
			'group_list' => $groups,
3807
			'selected_member' => $memID,
3808
			'status_open' => 0,
3809
			'nonjoin_group_id' => 1,
3810
			'min_posts' => -1,
3811
			'moderator_group' => 3,
3812
		)
3813
	);
3814
	// This beast will be our group holder.
3815
	$context['groups'] = array(
3816
		'member' => array(),
3817
		'available' => array()
3818
	);
3819
	while ($row = $smcFunc['db_fetch_assoc']($request))
3820
	{
3821
		// Can they edit their primary group?
3822
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
3823
			$context['can_edit_primary'] = true;
3824
3825
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
3826
		if (((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0)) && $row['id_group'] != $context['primary_group'])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! $context['can_manage_...ontext['primary_group'], Probably Intended Meaning: ! $context['can_manage_p...ntext['primary_group'])
Loading history...
3827
			continue;
3828
3829
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
3830
			'id' => $row['id_group'],
3831
			'name' => $row['group_name'],
3832
			'desc' => $row['description'],
3833
			'color' => $row['online_color'],
3834
			'type' => $row['group_type'],
3835
			'pending' => $row['pending'],
3836
			'is_primary' => $row['id_group'] == $context['primary_group'],
3837
			'can_be_primary' => $row['hidden'] != 2,
3838
			// Anything more than this needs to be done through account settings for security.
3839
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
3840
		);
3841
	}
3842
	$smcFunc['db_free_result']($request);
3843
3844
	// Add registered members on the end.
3845
	$context['groups']['member'][0] = array(
3846
		'id' => 0,
3847
		'name' => $txt['regular_members'],
3848
		'desc' => $txt['regular_members_desc'],
3849
		'type' => 0,
3850
		'is_primary' => $context['primary_group'] == 0 ? true : false,
3851
		'can_be_primary' => true,
3852
		'can_leave' => 0,
3853
	);
3854
3855
	// No changing primary one unless you have enough groups!
3856
	if (count($context['groups']['member']) < 2)
3857
		$context['can_edit_primary'] = false;
3858
3859
	// In the special case that someone is requesting membership of a group, setup some special context vars.
3860
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
3861
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
3862
}
3863
3864
/**
3865
 * This function actually makes all the group changes
3866
 *
3867
 * @param array $profile_vars The profile variables
3868
 * @param array $post_errors Any errors that have occurred
3869
 * @param int $memID The ID of the member
3870
 * @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
3871
 */
3872
function groupMembership2($profile_vars, $post_errors, $memID)
3873
{
3874
	global $user_info, $context, $user_profile, $modSettings, $smcFunc;
3875
3876
	// Let's be extra cautious...
3877
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
3878
		isAllowedTo('manage_membergroups');
3879
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
3880
		fatal_lang_error('no_access', false);
3881
3882
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
3883
3884
	$old_profile = &$user_profile[$memID];
3885
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3886
	$context['can_manage_protected'] = allowedTo('admin_forum');
3887
3888
	// By default the new primary is the old one.
3889
	$newPrimary = $old_profile['id_group'];
3890
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
3891
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
3892
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
3893
3894
	// One way or another, we have a target group in mind...
3895
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
3896
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
3897
3898
	// Sanity check!!
3899
	if ($group_id == 1)
3900
		isAllowedTo('admin_forum');
3901
	// Protected groups too!
3902
	else
3903
	{
3904
		$request = $smcFunc['db_query']('', '
3905
			SELECT group_type
3906
			FROM {db_prefix}membergroups
3907
			WHERE id_group = {int:current_group}
3908
			LIMIT {int:limit}',
3909
			array(
3910
				'current_group' => $group_id,
3911
				'limit' => 1,
3912
			)
3913
		);
3914
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
3915
		$smcFunc['db_free_result']($request);
3916
3917
		if ($is_protected == 1)
3918
			isAllowedTo('admin_forum');
3919
	}
3920
3921
	// What ever we are doing, we need to determine if changing primary is possible!
3922
	$request = $smcFunc['db_query']('', '
3923
		SELECT id_group, group_type, hidden, group_name
3924
		FROM {db_prefix}membergroups
3925
		WHERE id_group IN ({int:group_list}, {int:current_group})',
3926
		array(
3927
			'group_list' => $group_id,
3928
			'current_group' => $old_profile['id_group'],
3929
		)
3930
	);
3931
	while ($row = $smcFunc['db_fetch_assoc']($request))
3932
	{
3933
		// Is this the new group?
3934
		if ($row['id_group'] == $group_id)
3935
		{
3936
			$foundTarget = true;
3937
			$group_name = $row['group_name'];
3938
3939
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
3940
			if ($changeType == 'request' && $row['group_type'] != 2)
3941
				fatal_lang_error('no_access', false);
3942
			// What about leaving a requestable group we are not a member of?
3943
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
3944
				fatal_lang_error('no_access', false);
3945
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
3946
				fatal_lang_error('no_access', false);
3947
3948
			// We can't change the primary group if this is hidden!
3949
			if ($row['hidden'] == 2)
3950
				$canChangePrimary = false;
3951
		}
3952
3953
		// If this is their old primary, can we change it?
3954
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
3955
			$canChangePrimary = 1;
3956
3957
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
3958
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
3959
			$canChangePrimary = false;
3960
3961
		// If this is the one we are acting on, can we even act?
3962
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
3963
			$canChangePrimary = false;
3964
	}
3965
	$smcFunc['db_free_result']($request);
3966
3967
	// Didn't find the target?
3968
	if (!$foundTarget)
3969
		fatal_lang_error('no_access', false);
3970
3971
	// Final security check, don't allow users to promote themselves to admin.
3972
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
3973
	{
3974
		$request = $smcFunc['db_query']('', '
3975
			SELECT COUNT(permission)
3976
			FROM {db_prefix}permissions
3977
			WHERE id_group = {int:selected_group}
3978
				AND permission = {string:admin_forum}
3979
				AND add_deny = {int:not_denied}',
3980
			array(
3981
				'selected_group' => $group_id,
3982
				'not_denied' => 1,
3983
				'admin_forum' => 'admin_forum',
3984
			)
3985
		);
3986
		list ($disallow) = $smcFunc['db_fetch_row']($request);
3987
		$smcFunc['db_free_result']($request);
3988
3989
		if ($disallow)
3990
			isAllowedTo('admin_forum');
3991
	}
3992
3993
	// If we're requesting, add the note then return.
3994
	if ($changeType == 'request')
3995
	{
3996
		$request = $smcFunc['db_query']('', '
3997
			SELECT id_member
3998
			FROM {db_prefix}log_group_requests
3999
			WHERE id_member = {int:selected_member}
4000
				AND id_group = {int:selected_group}
4001
				AND status = {int:status_open}',
4002
			array(
4003
				'selected_member' => $memID,
4004
				'selected_group' => $group_id,
4005
				'status_open' => 0,
4006
			)
4007
		);
4008
		if ($smcFunc['db_num_rows']($request) != 0)
4009
			fatal_lang_error('profile_error_already_requested_group');
4010
		$smcFunc['db_free_result']($request);
4011
4012
		// Log the request.
4013
		$smcFunc['db_insert']('',
4014
			'{db_prefix}log_group_requests',
4015
			array(
4016
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
4017
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
4018
			),
4019
			array(
4020
				$memID, $group_id, time(), $_POST['reason'],
4021
				0, 0, '', 0, '',
4022
			),
4023
			array('id_request')
4024
		);
4025
4026
		// Set up some data for our background task...
4027
		$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
Comprehensibility Best Practice introduced by
The variable $group_name does not seem to be defined for all execution paths leading up to this point.
Loading history...
4028
4029
		// Add a background task to handle notifying people of this request
4030
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
4031
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
4032
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
4033
		);
4034
4035
		return $changeType;
4036
	}
4037
	// Otherwise we are leaving/joining a group.
4038
	elseif ($changeType == 'free')
4039
	{
4040
		// Are we leaving?
4041
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
4042
		{
4043
			if ($old_profile['id_group'] == $group_id)
4044
				$newPrimary = 0;
4045
			else
4046
				unset($addGroups[$group_id]);
4047
		}
4048
		// ... if not, must be joining.
4049
		else
4050
		{
4051
			// Can we change the primary, and do we want to?
4052
			if ($canChangePrimary)
4053
			{
4054
				if ($old_profile['id_group'] != 0)
4055
					$addGroups[$old_profile['id_group']] = -1;
4056
				$newPrimary = $group_id;
4057
			}
4058
			// Otherwise it's an additional group...
4059
			else
4060
				$addGroups[$group_id] = -1;
4061
		}
4062
	}
4063
	// Finally, we must be setting the primary.
4064
	elseif ($canChangePrimary)
4065
	{
4066
		if ($old_profile['id_group'] != 0)
4067
			$addGroups[$old_profile['id_group']] = -1;
4068
		if (isset($addGroups[$group_id]))
4069
			unset($addGroups[$group_id]);
4070
		$newPrimary = $group_id;
4071
	}
4072
4073
	// Finally, we can make the changes!
4074
	foreach ($addGroups as $id => $dummy)
4075
		if (empty($id))
4076
			unset($addGroups[$id]);
4077
	$addGroups = implode(',', array_flip($addGroups));
4078
4079
	// Ensure that we don't cache permissions if the group is changing.
4080
	if ($context['user']['is_owner'])
4081
		$_SESSION['mc']['time'] = 0;
4082
	else
4083
		updateSettings(array('settings_updated' => time()));
4084
4085
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
4086
4087
	return $changeType;
4088
}
4089
4090
/**
4091
 * Provides interface to setup Two Factor Auth in SMF
4092
 *
4093
 * @param int $memID The ID of the member
4094
 */
4095
function tfasetup($memID)
4096
{
4097
	global $user_info, $context, $user_settings, $sourcedir, $modSettings, $smcFunc;
4098
4099
	require_once($sourcedir . '/Class-TOTP.php');
4100
	require_once($sourcedir . '/Subs-Auth.php');
4101
4102
	// load JS lib for QR
4103
	loadJavaScriptFile('qrcode.js', array('force_current' => false, 'validate' => true));
4104
4105
	// If TFA has not been setup, allow them to set it up
4106
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4107
	{
4108
		// Check to ensure we're forcing SSL for authentication
4109
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $maintenance seems to never exist and therefore empty should always be true.
Loading history...
4110
			fatal_lang_error('login_ssl_required');
4111
4112
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4113
		// we do not want too much AJAX to confuse them.
4114
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4115
		{
4116
			$context['from_ajax'] = true;
4117
			$context['template_layers'] = array();
4118
		}
4119
4120
		// When the code is being sent, verify to make sure the user got it right
4121
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4122
		{
4123
			$code = $_POST['tfa_code'];
4124
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4125
			$totp->setRange(1);
4126
			$valid_password = hash_verify_password($user_settings['member_name'], trim($_POST['passwd']), $user_settings['passwd']);
4127
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4128
4129
			if ($valid_password && $valid_code)
4130
			{
4131
				$backup = substr(sha1($smcFunc['random_int']()), 0, 16);
4132
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4133
4134
				updateMemberData($memID, array(
4135
					'tfa_secret' => $_SESSION['tfa_secret'],
4136
					'tfa_backup' => $backup_encrypted,
4137
				));
4138
4139
				setTFACookie(3153600, $memID, hash_salt($backup_encrypted, $user_settings['password_salt']));
4140
4141
				unset($_SESSION['tfa_secret']);
4142
4143
				$context['tfa_backup'] = $backup;
4144
				$context['sub_template'] = 'tfasetup_backup';
4145
4146
				return;
4147
			}
4148
			else
4149
			{
4150
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4151
				$context['tfa_error'] = !$valid_code;
4152
				$context['tfa_pass_error'] = !$valid_password;
4153
				$context['tfa_pass_value'] = $_POST['passwd'];
4154
				$context['tfa_value'] = $_POST['tfa_code'];
4155
			}
4156
		}
4157
		else
4158
		{
4159
			$totp = new \TOTP\Auth();
4160
			$secret = $totp->generateCode();
4161
			$_SESSION['tfa_secret'] = $secret;
4162
			$context['tfa_secret'] = $secret;
4163
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4164
		}
4165
4166
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4167
	}
4168
	else
4169
		redirectexit('action=profile;area=account;u=' . $memID);
4170
}
4171
4172
/**
4173
 * Provides interface to disable two-factor authentication in SMF
4174
 *
4175
 * @param int $memID The ID of the member
4176
 */
4177
function tfadisable($memID)
4178
{
4179
	global $context, $modSettings, $smcFunc, $user_settings;
4180
4181
	if (!empty($user_settings['tfa_secret']))
4182
	{
4183
		// Bail if we're forcing SSL for authentication and the network connection isn't secure.
4184
		if (!empty($modSettings['force_ssl']) && !httpsOn())
4185
			fatal_lang_error('login_ssl_required', false);
4186
4187
		// The admin giveth...
4188
		elseif ($modSettings['tfa_mode'] == 3 && $context['user']['is_owner'])
4189
			fatal_lang_error('cannot_disable_tfa', false);
4190
		elseif ($modSettings['tfa_mode'] == 2 && $context['user']['is_owner'])
4191
		{
4192
			$groups = array($user_settings['id_group']);
4193
			if (!empty($user_settings['additional_groups']))
4194
				$groups = array_unique(array_merge($groups, explode(',', $user_settings['additional_groups'])));
4195
4196
			$request = $smcFunc['db_query']('', '
4197
				SELECT id_group
4198
				FROM {db_prefix}membergroups
4199
				WHERE tfa_required = {int:tfa_required}
4200
					AND id_group IN ({array_int:groups})',
4201
				array(
4202
					'tfa_required' => 1,
4203
					'groups' => $groups,
4204
				)
4205
			);
4206
			// They belong to a membergroup that requires tfa.
4207
			if (!empty($smcFunc['db_num_rows']($request)))
4208
				fatal_lang_error('cannot_disable_tfa2', false);
4209
			$smcFunc['db_free_result']($request);
4210
		}
4211
	}
4212
	else
4213
		redirectexit('action=profile;area=account;u=' . $memID);
4214
}
4215
4216
?>