Passed
Pull Request — release-2.1 (#4834)
by Jeremy
05:22
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_post' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'approve_posts', 'is_board' => true)),
1946
			'msg_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1947
			'msg_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1948
			'member_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1949
			'member_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1950
		),
1951
		'members' => array(
1952
			'member_register' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1953
			'request_group' => array('alert' => 'yes', 'email' => 'yes'),
1954
			'warn_any' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'issue_warning', 'is_board' => false)),
1955
			'buddy_request'  => array('alert' => 'yes', 'email' => 'never'),
1956
			'birthday'  => array('alert' => 'yes', 'email' => 'yes'),
1957
		),
1958
		'calendar' => array(
1959
			'event_new' => array('alert' => 'yes', 'email' => 'yes', 'help' => 'alert_event_new'),
1960
		),
1961
		'paidsubs' => array(
1962
			'paidsubs_expiring' => array('alert' => 'yes', 'email' => 'yes'),
1963
		),
1964
	);
1965
	$group_options = array(
1966
		'board' => array(
1967
			array('check', 'msg_auto_notify', 'label' => 'after'),
1968
			array('check', 'msg_receive_body', 'label' => 'after'),
1969
			array('select', 'msg_notify_pref', 'label' => 'before', 'opts' => array(
1970
				0 => $txt['alert_opt_msg_notify_pref_nothing'],
1971
				1 => $txt['alert_opt_msg_notify_pref_instant'],
1972
				2 => $txt['alert_opt_msg_notify_pref_first'],
1973
				3 => $txt['alert_opt_msg_notify_pref_daily'],
1974
				4 => $txt['alert_opt_msg_notify_pref_weekly'],
1975
			)),
1976
			array('select', 'msg_notify_type', 'label' => 'before', 'opts' => array(
1977
				1 => $txt['notify_send_type_everything'],
1978
				2 => $txt['notify_send_type_everything_own'],
1979
				3 => $txt['notify_send_type_only_replies'],
1980
				4 => $txt['notify_send_type_nothing'],
1981
			)),
1982
		),
1983
		'pm' => array(
1984
			array('select', 'pm_notify', 'label' => 'before', 'opts' => array(
1985
				1 => $txt['email_notify_all'],
1986
				2 => $txt['email_notify_buddies'],
1987
			)),
1988
		),
1989
	);
1990
1991
	// There are certain things that are disabled at the group level.
1992
	if (empty($modSettings['cal_enabled']))
1993
		unset($alert_types['calendar']);
1994
1995
	// Disable paid subscriptions at group level if they're disabled
1996
	if (empty($modSettings['paid_enabled']))
1997
		unset($alert_types['paidsubs']);
1998
1999
	// Disable membergroup requests at group level if they're disabled
2000
	if (empty($modSettings['show_group_membership']))
2001
		unset($alert_types['groupr'], $alert_types['members']['request_group']);
2002
2003
	// Disable mentions if they're disabled
2004
	if (empty($modSettings['enable_mentions']))
2005
		unset($alert_types['msg']['msg_mention']);
2006
2007
	// Disable likes if they're disabled
2008
	if (empty($modSettings['enable_likes']))
2009
		unset($alert_types['msg']['msg_like']);
2010
2011
	// Disable buddy requests if they're disabled
2012
	if (empty($modSettings['enable_buddylist']))
2013
		unset($alert_types['members']['buddy_request']);
2014
2015
	// Now, now, we could pass this through global but we should really get into the habit of
2016
	// passing content to hooks, not expecting hooks to splatter everything everywhere.
2017
	call_integration_hook('integrate_alert_types', array(&$alert_types, &$group_options));
2018
2019
	// Now we have to do some permissions testing - but only if we're not loading this from the admin center
2020
	if (!empty($memID))
2021
	{
2022
		require_once($sourcedir . '/Subs-Members.php');
2023
		$perms_cache = array();
2024
		$request = $smcFunc['db_query']('', '
2025
			SELECT COUNT(*)
2026
			FROM {db_prefix}group_moderators
2027
			WHERE id_member = {int:memID}',
2028
			array(
2029
				'memID' => $memID,
2030
			)
2031
		);
2032
2033
		list ($can_mod) = $smcFunc['db_fetch_row']($request);
2034
2035
		if (!isset($perms_cache['manage_membergroups']))
2036
		{
2037
			$members = membersAllowedTo('manage_membergroups');
2038
			$perms_cache['manage_membergroups'] = in_array($memID, $members);
2039
		}
2040
2041
		if (!($perms_cache['manage_membergroups'] || $can_mod != 0))
2042
			unset($alert_types['members']['request_group']);
2043
2044
		foreach ($alert_types as $group => $items)
2045
		{
2046
			foreach ($items as $alert_key => $alert_value)
2047
			{
2048
				if (!isset($alert_value['permission']))
2049
					continue;
2050
				if (!isset($perms_cache[$alert_value['permission']['name']]))
2051
				{
2052
					$in_board = !empty($alert_value['permission']['is_board']) ? 0 : null;
2053
					$members = membersAllowedTo($alert_value['permission']['name'], $in_board);
2054
					$perms_cache[$alert_value['permission']['name']] = in_array($memID, $members);
2055
				}
2056
2057
				if (!$perms_cache[$alert_value['permission']['name']])
2058
					unset ($alert_types[$group][$alert_key]);
2059
			}
2060
2061
			if (empty($alert_types[$group]))
2062
				unset ($alert_types[$group]);
2063
		}
2064
	}
2065
2066
	// And finally, exporting it to be useful later.
2067
	$context['alert_types'] = $alert_types;
2068
	$context['alert_group_options'] = $group_options;
2069
2070
	$context['alert_bits'] = array(
2071
		'alert' => 0x01,
2072
		'email' => 0x02,
2073
	);
2074
2075
	if (isset($_POST['notify_submit']))
2076
	{
2077
		checkSession();
2078
		validateToken($context['token_check'], 'post');
2079
2080
		// We need to step through the list of valid settings and figure out what the user has set.
2081
		$update_prefs = array();
2082
2083
		// Now the group level options
2084
		foreach ($context['alert_group_options'] as $opt_group => $group)
2085
		{
2086
			foreach ($group as $this_option)
2087
			{
2088
				switch ($this_option[0])
2089
				{
2090
					case 'check':
2091
						$update_prefs[$this_option[1]] = !empty($_POST['opt_' . $this_option[1]]) ? 1 : 0;
2092
						break;
2093
					case 'select':
2094
						if (isset($_POST['opt_' . $this_option[1]], $this_option['opts'][$_POST['opt_' . $this_option[1]]]))
2095
							$update_prefs[$this_option[1]] = $_POST['opt_' . $this_option[1]];
2096
						else
2097
						{
2098
							// We didn't have a sane value. Let's grab the first item from the possibles.
2099
							$keys = array_keys($this_option['opts']);
2100
							$first = array_shift($keys);
2101
							$update_prefs[$this_option[1]] = $first;
2102
						}
2103
						break;
2104
				}
2105
			}
2106
		}
2107
2108
		// Now the individual options
2109
		foreach ($context['alert_types'] as $alert_group => $items)
2110
		{
2111
			foreach ($items as $item_key => $this_options)
2112
			{
2113
				$this_value = 0;
2114
				foreach ($context['alert_bits'] as $type => $bitvalue)
2115
				{
2116
					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...
2117
						$this_value |= $bitvalue;
2118
				}
2119
				if (!isset($context['alert_prefs'][$item_key]) || $context['alert_prefs'][$item_key] != $this_value)
2120
					$update_prefs[$item_key] = $this_value;
2121
			}
2122
		}
2123
2124
		if (!empty($_POST['opt_alert_timeout']))
2125
			$update_prefs['alert_timeout'] = $context['member']['alert_timeout'] = (int) $_POST['opt_alert_timeout'];
2126
2127
		if (!empty($_POST['notify_announcements']))
2128
			$update_prefs['announcements'] = $context['member']['notify_announcements'] = (int) $_POST['notify_announcements'];
2129
2130
		setNotifyPrefs((int) $memID, $update_prefs);
2131
		foreach ($update_prefs as $pref => $value)
2132
			$context['alert_prefs'][$pref] = $value;
2133
2134
		makeNotificationChanges($memID);
2135
2136
		$context['profile_updated'] = $txt['profile_updated_own'];
2137
	}
2138
2139
	createToken($context['token_check'], 'post');
2140
}
2141
2142
/**
2143
 * Marks all alerts as read for the specified user
2144
 *
2145
 * @param int $memID The ID of the member
2146
 */
2147
function alert_markread($memID)
2148
{
2149
	global $context, $db_show_debug, $smcFunc;
2150
2151
	// We do not want to output debug information here.
2152
	$db_show_debug = false;
2153
2154
	// We only want to output our little layer here.
2155
	$context['template_layers'] = array();
2156
	$context['sub_template'] = 'alerts_all_read';
2157
2158
	loadLanguage('Alerts');
2159
2160
	// Now we're all set up.
2161
	is_not_guest();
2162
	if (!$context['user']['is_owner'])
2163
		fatal_error('no_access');
2164
2165
	checkSession('get');
2166
2167
	// Assuming we're here, mark everything as read and head back.
2168
	// We only spit back the little layer because this should be called AJAXively.
2169
	$smcFunc['db_query']('', '
2170
		UPDATE {db_prefix}user_alerts
2171
		SET is_read = {int:now}
2172
		WHERE id_member = {int:current_member}
2173
			AND is_read = 0',
2174
		array(
2175
			'now' => time(),
2176
			'current_member' => $memID,
2177
		)
2178
	);
2179
2180
	updateMemberData($memID, array('alerts' => 0));
2181
}
2182
2183
/**
2184
 * Marks a group of alerts as un/read
2185
 *
2186
 * @param int $memID The user ID.
2187
 * @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.
2188
 * @param integer $read To mark as read or unread, 1 for read, 0 or any other value different than 1 for unread.
2189
 * @return integer How many alerts remain unread
2190
 */
2191
function alert_mark($memID, $toMark, $read = 0)
2192
{
2193
	global $smcFunc;
2194
2195
	if (empty($toMark) || empty($memID))
2196
		return false;
2197
2198
	$toMark = (array) $toMark;
2199
2200
	$smcFunc['db_query']('', '
2201
		UPDATE {db_prefix}user_alerts
2202
		SET is_read = {int:read}
2203
		WHERE id_alert IN({array_int:toMark})',
2204
		array(
2205
			'read' => $read == 1 ? time() : 0,
2206
			'toMark' => $toMark,
2207
		)
2208
	);
2209
2210
	// Gotta know how many unread alerts are left.
2211
	$count = alert_count($memID, true);
2212
2213
	updateMemberData($memID, array('alerts' => $count));
2214
2215
	// Might want to know this.
2216
	return $count;
2217
}
2218
2219
/**
2220
 * Deletes a single or a group of alerts by ID
2221
 *
2222
 * @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...
2223
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2224
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
2225
 */
2226
function alert_delete($toDelete, $memID = false)
2227
{
2228
	global $smcFunc;
2229
2230
	if (empty($toDelete))
2231
		return false;
2232
2233
	$toDelete = (array) $toDelete;
2234
2235
	$smcFunc['db_query']('', '
2236
		DELETE FROM {db_prefix}user_alerts
2237
		WHERE id_alert IN({array_int:toDelete})',
2238
		array(
2239
			'toDelete' => $toDelete,
2240
		)
2241
	);
2242
2243
	// Gotta know how many unread alerts are left.
2244
	if ($memID)
2245
	{
2246
		$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

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

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

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