Passed
Push — release-2.1 ( 6d8394...7401ba )
by Mathias
08:01
created

alert_count()   F

Complexity

Conditions 25
Paths 513

Size

Total Lines 160
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 25
eloc 74
nc 513
nop 2
dl 0
loc 160
rs 0.6763
c 3
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has the primary job of showing and editing people's profiles.
5
 * 	It also allows the user to change some of their or another's preferences,
6
 * 	and such things
7
 *
8
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines https://www.simplemachines.org
12
 * @copyright 2022 Simple Machines and individual contributors
13
 * @license https://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1.2
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, $settings;
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'] : smf_strftime('%Y-%m-%d', $cur_profile['date_registered']),
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 $modSettings 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...
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)) === false)
148
				{
149
					$value = $cur_profile['date_registered'];
150
					return $txt['invalid_registration'] . ' ' . smf_strftime('%d %b %Y ' . (strpos($user_info['time_format'], '%H') !== false ? '%I:%M:%S %p' : '%H:%M:%S'), time());
151
				}
152
153
				// As long as it doesn't equal "N/A"...
154
				elseif ($value != $txt['not_applicable'] && $value != strtotime(smf_strftime('%Y-%m-%d', $cur_profile['date_registered'])))
155
				{
156
					$diff = $cur_profile['date_registered'] - strtotime(smf_strftime('%Y-%m-%d', $cur_profile['date_registered']));
157
					$value = $value + $diff;
158
				}
159
160
				else
161
					$value = $cur_profile['date_registered'];
162
163
				return true;
164
			},
165
		),
166
		'email_address' => array(
167
			'type' => 'email',
168
			'label' => $txt['user_email_address'],
169
			'subtext' => $txt['valid_email'],
170
			'log_change' => true,
171
			'permission' => 'profile_password',
172
			'js_submit' => !empty($modSettings['send_validation_onChange']) ? '
173
	form_handle.addEventListener("submit", function(event)
174
	{
175
		if (this.email_address.value != "' . (!empty($cur_profile['email_address']) ? $cur_profile['email_address'] : '') . '")
176
		{
177
			alert(' . JavaScriptEscape($txt['email_change_logout']) . ');
178
			return true;
179
		}
180
	}, false);' : '',
181
			'input_validate' => function(&$value)
182
			{
183
				global $context, $old_profile, $profile_vars, $sourcedir, $modSettings;
184
185
				if (strtolower($value) == strtolower($old_profile['email_address']))
186
					return false;
187
188
				$isValid = profileValidateEmail($value, $context['id_member']);
189
190
				// Do they need to revalidate? If so schedule the function!
191
				if ($isValid === true && !empty($modSettings['send_validation_onChange']) && !allowedTo('moderate_forum'))
192
				{
193
					require_once($sourcedir . '/Subs-Members.php');
194
					$profile_vars['validation_code'] = generateValidationCode();
195
					$profile_vars['is_activated'] = 2;
196
					$context['profile_execute_on_save'][] = 'profileSendActivation';
197
					unset($context['profile_execute_on_save']['reload_user']);
198
				}
199
200
				return $isValid;
201
			},
202
		),
203
		// Selecting group membership is a complicated one so we treat it separate!
204
		'id_group' => array(
205
			'type' => 'callback',
206
			'callback_func' => 'group_manage',
207
			'permission' => 'manage_membergroups',
208
			'preload' => 'profileLoadGroups',
209
			'log_change' => true,
210
			'input_validate' => 'profileSaveGroups',
211
		),
212
		'id_theme' => array(
213
			'type' => 'callback',
214
			'callback_func' => 'theme_pick',
215
			'permission' => 'profile_extra',
216
			'enabled' => $modSettings['theme_allow'] || allowedTo('admin_forum'),
217
			'preload' => function() use ($smcFunc, &$context, $cur_profile, $txt)
218
			{
219
				$request = $smcFunc['db_query']('', '
220
					SELECT value
221
					FROM {db_prefix}themes
222
					WHERE id_theme = {int:id_theme}
223
						AND variable = {string:variable}
224
					LIMIT 1', array(
225
						'id_theme' => $cur_profile['id_theme'],
226
						'variable' => 'name',
227
					)
228
				);
229
				list ($name) = $smcFunc['db_fetch_row']($request);
230
				$smcFunc['db_free_result']($request);
231
232
				$context['member']['theme'] = array(
233
					'id' => $cur_profile['id_theme'],
234
					'name' => empty($cur_profile['id_theme']) ? $txt['theme_forum_default'] : $name
235
				);
236
				return true;
237
			},
238
			'input_validate' => function(&$value)
239
			{
240
				$value = (int) $value;
241
				return true;
242
			},
243
		),
244
		'lngfile' => array(
245
			'type' => 'select',
246
			'options' => function() use (&$context)
247
			{
248
				return $context['profile_languages'];
249
			},
250
			'label' => $txt['preferred_language'],
251
			'permission' => 'profile_identity',
252
			'preload' => 'profileLoadLanguages',
253
			'enabled' => !empty($modSettings['userLanguage']),
254
			'value' => empty($cur_profile['lngfile']) ? $language : $cur_profile['lngfile'],
255
			'input_validate' => function(&$value) use (&$context, $cur_profile)
256
			{
257
				// Load the languages.
258
				profileLoadLanguages();
259
260
				if (isset($context['profile_languages'][$value]))
261
				{
262
					if ($context['user']['is_owner'] && empty($context['password_auth_failed']))
263
						$_SESSION['language'] = $value;
264
					return true;
265
				}
266
				else
267
				{
268
					$value = $cur_profile['lngfile'];
269
					return false;
270
				}
271
			},
272
		),
273
		// The username is not always editable - so adjust it as such.
274
		'member_name' => array(
275
			'type' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? 'text' : 'label',
276
			'label' => $txt['username'],
277
			'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>]' : '',
278
			'log_change' => true,
279
			'permission' => 'profile_identity',
280
			'prehtml' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? '<div class="alert">' . $txt['username_warning'] . '</div>' : '',
281
			'input_validate' => function(&$value) use ($sourcedir, $context, $user_info, $cur_profile)
282
			{
283
				if (allowedTo('admin_forum'))
284
				{
285
					// We'll need this...
286
					require_once($sourcedir . '/Subs-Auth.php');
287
288
					// Maybe they are trying to change their password as well?
289
					$resetPassword = true;
290
					if (isset($_POST['passwrd1']) && $_POST['passwrd1'] != '' && isset($_POST['passwrd2']) && $_POST['passwrd1'] == $_POST['passwrd2'] && validatePassword(un_htmlspecialchars($_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(un_html..., $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...
291
						$resetPassword = false;
292
293
					// Do the reset... this will send them an email too.
294
					if ($resetPassword)
295
						resetPassword($context['id_member'], $value);
296
					elseif ($value !== null)
297
					{
298
						validateUsername($context['id_member'], trim(normalize_spaces(sanitize_chars($value, 1, ' '), true, true, array('no_breaks' => true, 'replace_tabs' => true, 'collapse_hspace' => true))));
299
						updateMemberData($context['id_member'], array('member_name' => $value));
300
301
						// 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)
302
						call_integration_hook('integrate_reset_pass', array($cur_profile['member_name'], $value, $_POST['passwrd1']));
303
					}
304
				}
305
				return false;
306
			},
307
		),
308
		'passwrd1' => array(
309
			'type' => 'password',
310
			'label' => $txt['choose_pass'],
311
			'subtext' => $txt['password_strength'],
312
			'size' => 20,
313
			'value' => '',
314
			'permission' => 'profile_password',
315
			'save_key' => 'passwd',
316
			// Note this will only work if passwrd2 also exists!
317
			'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...
318
			{
319
				// If we didn't try it then ignore it!
320
				if ($value == '')
321
					return false;
322
323
				// Do the two entries for the password even match?
324
				if (!isset($_POST['passwrd2']) || $value != $_POST['passwrd2'])
325
					return 'bad_new_password';
326
327
				// Let's get the validation function into play...
328
				require_once($sourcedir . '/Subs-Auth.php');
329
				$passwordErrors = validatePassword(un_htmlspecialchars($value), $cur_profile['member_name'], array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email']));
330
331
				// Were there errors?
332
				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...
333
					return 'password_' . $passwordErrors;
334
335
				// Set up the new password variable... ready for storage.
336
				$value = hash_password($cur_profile['member_name'], un_htmlspecialchars($value));
337
338
				return true;
339
			},
340
		),
341
		'passwrd2' => array(
342
			'type' => 'password',
343
			'label' => $txt['verify_pass'],
344
			'size' => 20,
345
			'value' => '',
346
			'permission' => 'profile_password',
347
			'is_dummy' => true,
348
		),
349
		'personal_text' => array(
350
			'type' => 'text',
351
			'label' => $txt['personal_text'],
352
			'log_change' => true,
353
			'input_attr' => array('maxlength="50"'),
354
			'size' => 50,
355
			'permission' => 'profile_blurb',
356
			'input_validate' => function(&$value) use ($smcFunc)
357
			{
358
				if ($smcFunc['strlen']($value) > 50)
359
					return 'personal_text_too_long';
360
361
				return true;
362
			},
363
		),
364
		// This does ALL the pm settings
365
		'pm_prefs' => array(
366
			'type' => 'callback',
367
			'callback_func' => 'pm_settings',
368
			'permission' => 'pm_read',
369
			'preload' => function() use (&$context, $cur_profile)
370
			{
371
				$context['display_mode'] = $cur_profile['pm_prefs'] & 3;
372
				$context['receive_from'] = !empty($cur_profile['pm_receive_from']) ? $cur_profile['pm_receive_from'] : 0;
373
374
				return true;
375
			},
376
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
377
			{
378
				// Simple validate and apply the two "sub settings"
379
				$value = max(min($value, 2), 0);
380
381
				$cur_profile['pm_receive_from'] = $profile_vars['pm_receive_from'] = max(min((int) $_POST['pm_receive_from'], 4), 0);
382
383
				return true;
384
			},
385
		),
386
		'posts' => array(
387
			'type' => 'int',
388
			'label' => $txt['profile_posts'],
389
			'log_change' => true,
390
			'size' => 7,
391
			'min' => 0,
392
			'max' => 2 ** 24 - 1,
393
			'permission' => 'moderate_forum',
394
			'input_validate' => function(&$value)
395
			{
396
				if (!is_numeric($value))
397
					return 'digits_only';
398
				elseif ($value < 0 || $value > 2 ** 24 - 1)
399
					return 'posts_out_of_range';
400
				else
401
					$value = $value != '' ? strtr($value, array(',' => '', '.' => '', ' ' => '')) : 0;
402
				return true;
403
			},
404
		),
405
		'real_name' => array(
406
			'type' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum') ? 'text' : 'label',
407
			'label' => $txt['name'],
408
			'subtext' => $txt['display_name_desc'],
409
			'log_change' => true,
410
			'input_attr' => array('maxlength="60"'),
411
			'permission' => 'profile_displayed_name',
412
			'enabled' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum'),
413
			'input_validate' => function(&$value) use ($context, $smcFunc, $sourcedir, $cur_profile)
414
			{
415
				$value = trim(normalize_spaces(sanitize_chars($value, 1, ' '), true, true, array('no_breaks' => true, 'replace_tabs' => true, 'collapse_hspace' => true)));
416
417
				if (trim($value) == '')
418
					return 'no_name';
419
				elseif ($smcFunc['strlen']($value) > 60)
420
					return 'name_too_long';
421
				elseif ($cur_profile['real_name'] != $value)
422
				{
423
					require_once($sourcedir . '/Subs-Members.php');
424
					if (isReservedName($value, $context['id_member']))
425
						return 'name_taken';
426
				}
427
				return true;
428
			},
429
		),
430
		'secret_question' => array(
431
			'type' => 'text',
432
			'label' => $txt['secret_question'],
433
			'subtext' => $txt['secret_desc'],
434
			'size' => 50,
435
			'permission' => 'profile_password',
436
		),
437
		'secret_answer' => array(
438
			'type' => 'text',
439
			'label' => $txt['secret_answer'],
440
			'subtext' => $txt['secret_desc2'],
441
			'size' => 20,
442
			'postinput' => '<span class="smalltext"><a href="' . $scripturl . '?action=helpadmin;help=secret_why_blank" onclick="return reqOverlayDiv(this.href);"><span class="main_icons help"></span> ' . $txt['secret_why_blank'] . '</a></span>',
443
			'value' => '',
444
			'permission' => 'profile_password',
445
			'input_validate' => function(&$value) use ($cur_profile)
446
			{
447
				$value = $value != '' ? hash_password($cur_profile['member_name'], $value) : '';
448
				return true;
449
			},
450
		),
451
		'signature' => array(
452
			'type' => 'callback',
453
			'callback_func' => 'signature_modify',
454
			'permission' => 'profile_signature',
455
			'enabled' => substr($modSettings['signature_settings'], 0, 1) == 1,
456
			'preload' => 'profileLoadSignatureData',
457
			'input_validate' => 'profileValidateSignature',
458
		),
459
		'show_online' => array(
460
			'type' => 'check',
461
			'label' => $txt['show_online'],
462
			'permission' => 'profile_identity',
463
			'enabled' => !empty($modSettings['allow_hideOnline']) || allowedTo('moderate_forum'),
464
		),
465
		'smiley_set' => array(
466
			'type' => 'callback',
467
			'callback_func' => 'smiley_pick',
468
			'enabled' => !empty($modSettings['smiley_sets_enable']),
469
			'permission' => 'profile_extra',
470
			'preload' => function() use ($modSettings, &$context, &$txt, $cur_profile, $smcFunc, $settings, $language)
471
			{
472
				$context['member']['smiley_set']['id'] = empty($cur_profile['smiley_set']) ? '' : $cur_profile['smiley_set'];
473
				$context['smiley_sets'] = explode(',', 'none,,' . $modSettings['smiley_sets_known']);
474
				$set_names = explode("\n", $txt['smileys_none'] . "\n" . $txt['smileys_forum_board_default'] . "\n" . $modSettings['smiley_sets_names']);
475
476
				$filenames = array();
477
				$result = $smcFunc['db_query']('', '
478
					SELECT f.filename, f.smiley_set
479
					FROM {db_prefix}smiley_files AS f
480
						JOIN {db_prefix}smileys AS s ON (s.id_smiley = f.id_smiley)
481
					WHERE s.code = {string:smiley}',
482
					array(
483
						'smiley' => ':)',
484
					)
485
				);
486
				while ($row = $smcFunc['db_fetch_assoc']($result))
487
					$filenames[$row['smiley_set']] = $row['filename'];
488
				$smcFunc['db_free_result']($result);
489
490
				// In case any sets don't contain a ':)' smiley
491
				$no_smiley_sets = array_diff(explode(',', $modSettings['smiley_sets_known']), array_keys($filenames));
492
				foreach ($no_smiley_sets as $set)
493
				{
494
					$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
495
					$images = glob(implode('/', array($modSettings['smileys_dir'], $set, '*.{' . (implode(',', $allowedTypes) . '}'))), GLOB_BRACE);
496
497
					// Just use some image or other
498
					if (!empty($images))
499
					{
500
						$image = array_pop($images);
501
						$filenames[$set] = pathinfo($image, PATHINFO_BASENAME);
502
					}
503
					// No images at all? That's no good. Let the admin know, and quietly skip for this user.
504
					else
505
					{
506
						loadLanguage('Errors', $language);
507
						log_error(sprintf($txt['smiley_set_dir_not_found'], $set_names[array_search($set, $context['smiley_sets'])]));
508
509
						$context['smiley_sets'] = array_filter($context['smiley_sets'], function($v) use ($set)
510
							{
511
								return $v != $set;
512
							});
513
					}
514
				}
515
516
				foreach ($context['smiley_sets'] as $i => $set)
517
				{
518
					$context['smiley_sets'][$i] = array(
519
						'id' => $smcFunc['htmlspecialchars']($set),
520
						'name' => $smcFunc['htmlspecialchars']($set_names[$i]),
521
						'selected' => $set == $context['member']['smiley_set']['id']
522
					);
523
524
					if ($set === 'none')
525
						$context['smiley_sets'][$i]['preview'] = $settings['images_url'] . '/blank.png';
526
					elseif ($set === '')
527
					{
528
						$default_set = !empty($settings['smiley_sets_default']) ? $settings['smiley_sets_default'] : $modSettings['smiley_sets_default'];
529
						$context['smiley_sets'][$i]['preview'] = implode('/', array($modSettings['smileys_url'], $default_set, $filenames[$default_set]));
530
					}
531
					else
532
						$context['smiley_sets'][$i]['preview'] = implode('/', array($modSettings['smileys_url'], $set, $filenames[$set]));
533
534
					if ($context['smiley_sets'][$i]['selected'])
535
					{
536
						$context['member']['smiley_set']['name'] = $set_names[$i];
537
						$context['member']['smiley_set']['preview'] = $context['smiley_sets'][$i]['preview'];
538
					}
539
540
					$context['smiley_sets'][$i]['preview'] = $smcFunc['htmlspecialchars']($context['smiley_sets'][$i]['preview']);
541
				}
542
543
				return true;
544
			},
545
			'input_validate' => function(&$value)
546
			{
547
				global $modSettings;
548
549
				$smiley_sets = explode(',', $modSettings['smiley_sets_known']);
550
				if (!in_array($value, $smiley_sets) && $value != 'none')
551
					$value = '';
552
				return true;
553
			},
554
		),
555
		// Pretty much a dummy entry - it populates all the theme settings.
556
		'theme_settings' => array(
557
			'type' => 'callback',
558
			'callback_func' => 'theme_settings',
559
			'permission' => 'profile_extra',
560
			'is_dummy' => true,
561
			'preload' => function() use (&$context, $user_info, $modSettings)
562
			{
563
				loadLanguage('Settings');
564
565
				$context['allow_no_censored'] = false;
566
				if ($user_info['is_admin'] || $context['user']['is_owner'])
567
					$context['allow_no_censored'] = !empty($modSettings['allow_no_censored']);
568
569
				return true;
570
			},
571
		),
572
		'tfa' => array(
573
			'type' => 'callback',
574
			'callback_func' => 'tfa',
575
			'permission' => 'profile_password',
576
			'enabled' => !empty($modSettings['tfa_mode']),
577
			'preload' => function() use (&$context, $cur_profile)
578
			{
579
				$context['tfa_enabled'] = !empty($cur_profile['tfa_secret']);
580
581
				return true;
582
			},
583
		),
584
		'time_format' => array(
585
			'type' => 'callback',
586
			'callback_func' => 'timeformat_modify',
587
			'permission' => 'profile_extra',
588
			'preload' => function() use (&$context, $user_info, $txt, $cur_profile, $modSettings)
0 ignored issues
show
Unused Code introduced by
The import $user_info 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...
Unused Code introduced by
The import $modSettings 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...
589
			{
590
				$context['easy_timeformats'] = array(
591
					array('format' => '', 'title' => $txt['timeformat_default']),
592
					array('format' => '%B %d, %Y, %I:%M:%S %p', 'title' => $txt['timeformat_easy1']),
593
					array('format' => '%B %d, %Y, %H:%M:%S', 'title' => $txt['timeformat_easy2']),
594
					array('format' => '%Y-%m-%d, %H:%M:%S', 'title' => $txt['timeformat_easy3']),
595
					array('format' => '%d %B %Y, %H:%M:%S', 'title' => $txt['timeformat_easy4']),
596
					array('format' => '%d-%m-%Y, %H:%M:%S', 'title' => $txt['timeformat_easy5'])
597
				);
598
599
				$context['member']['time_format'] = $cur_profile['time_format'];
600
				$context['current_forum_time'] = timeformat(time(), false, 'forum');
601
				$context['current_forum_time_js'] = smf_strftime('%Y,' . ((int) smf_strftime('%m', time()) - 1) . ',%d,%H,%M,%S', time());
602
				$context['current_forum_time_hour'] = (int) smf_strftime('%H', time());
603
				return true;
604
			},
605
		),
606
		'timezone' => array(
607
			'type' => 'select',
608
			'options' => smf_list_timezones(),
609
			'disabled_options' => array_filter(array_keys(smf_list_timezones()), 'is_int'),
610
			'permission' => 'profile_extra',
611
			'label' => $txt['timezone'],
612
			'value' => empty($cur_profile['timezone']) ? $modSettings['default_timezone'] : $cur_profile['timezone'],
613
			'input_validate' => function($value)
614
			{
615
				$tz = smf_list_timezones();
616
				if (!isset($tz[$value]))
617
					return 'bad_timezone';
618
619
				return true;
620
			},
621
		),
622
		'usertitle' => array(
623
			'type' => 'text',
624
			'label' => $txt['custom_title'],
625
			'log_change' => true,
626
			'input_attr' => array('maxlength="50"'),
627
			'size' => 50,
628
			'permission' => 'profile_title',
629
			'enabled' => !empty($modSettings['titlesEnable']),
630
			'input_validate' => function(&$value) use ($smcFunc)
631
			{
632
				if ($smcFunc['strlen']($value) > 50)
633
					return 'user_title_too_long';
634
635
				return true;
636
			},
637
		),
638
		'website_title' => array(
639
			'type' => 'text',
640
			'label' => $txt['website_title'],
641
			'subtext' => $txt['include_website_url'],
642
			'size' => 50,
643
			'permission' => 'profile_website',
644
			'link_with' => 'website',
645
			'input_validate' => function(&$value) use ($smcFunc)
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...
646
			{
647
				if (mb_strlen($value) > 250)
648
					return 'website_title_too_long';
649
650
				return true;
651
			},
652
		),
653
		'website_url' => array(
654
			'type' => 'url',
655
			'label' => $txt['website_url'],
656
			'subtext' => $txt['complete_url'],
657
			'size' => 50,
658
			'permission' => 'profile_website',
659
			// Fix the URL...
660
			'input_validate' => function(&$value)
661
			{
662
				if (strlen(trim($value)) > 0 && strpos($value, '://') === false)
663
					$value = 'http://' . $value;
664
				if (strlen($value) < 8 || (substr($value, 0, 7) !== 'http://' && substr($value, 0, 8) !== 'https://'))
665
					$value = '';
666
				$value = (string) validate_iri(normalize_iri($value));
667
				return true;
668
			},
669
			'link_with' => 'website',
670
		),
671
	);
672
673
	call_integration_hook('integrate_load_profile_fields', array(&$profile_fields));
674
675
	$disabled_fields = !empty($modSettings['disabled_profile_fields']) ? explode(',', $modSettings['disabled_profile_fields']) : array();
676
	// For each of the above let's take out the bits which don't apply - to save memory and security!
677
	foreach ($profile_fields as $key => $field)
678
	{
679
		// Do we have permission to do this?
680
		if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? array($field['permission'] . '_own', $field['permission'] . '_any') : $field['permission'] . '_any')) && !allowedTo($field['permission']))
681
			unset($profile_fields[$key]);
682
683
		// Is it enabled?
684
		if (isset($field['enabled']) && !$field['enabled'])
685
			unset($profile_fields[$key]);
686
687
		// Is it specifically disabled?
688
		if (in_array($key, $disabled_fields) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields)))
689
			unset($profile_fields[$key]);
690
	}
691
}
692
693
/**
694
 * Setup the context for a page load!
695
 *
696
 * @param array $fields The profile fields to display. Each item should correspond to an item in the $profile_fields array generated by loadProfileFields
697
 */
698
function setupProfileContext($fields)
699
{
700
	global $profile_fields, $context, $cur_profile, $txt;
701
702
	// Some default bits.
703
	$context['profile_prehtml'] = '';
704
	$context['profile_posthtml'] = '';
705
	$context['profile_javascript'] = '';
706
	$context['profile_onsubmit_javascript'] = '';
707
708
	call_integration_hook('integrate_setup_profile_context', array(&$fields));
709
710
	// Make sure we have this!
711
	loadProfileFields(true);
712
713
	// First check for any linked sets.
714
	foreach ($profile_fields as $key => $field)
715
		if (isset($field['link_with']) && in_array($field['link_with'], $fields))
716
			$fields[] = $key;
717
718
	$i = 0;
719
	$last_type = '';
720
	foreach ($fields as $key => $field)
721
	{
722
		if (isset($profile_fields[$field]))
723
		{
724
			// Shortcut.
725
			$cur_field = &$profile_fields[$field];
726
727
			// Does it have a preload and does that preload succeed?
728
			if (isset($cur_field['preload']) && !$cur_field['preload']())
729
				continue;
730
731
			// If this is anything but complex we need to do more cleaning!
732
			if ($cur_field['type'] != 'callback' && $cur_field['type'] != 'hidden')
733
			{
734
				if (!isset($cur_field['label']))
735
					$cur_field['label'] = isset($txt[$field]) ? $txt[$field] : $field;
736
737
				// Everything has a value!
738
				if (!isset($cur_field['value']))
739
					$cur_field['value'] = isset($cur_profile[$field]) ? $cur_profile[$field] : '';
740
741
				// Any input attributes?
742
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
743
			}
744
745
			// Was there an error with this field on posting?
746
			if (isset($context['profile_errors'][$field]))
747
				$cur_field['is_error'] = true;
748
749
			// Any javascript stuff?
750
			if (!empty($cur_field['js_submit']))
751
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
752
			if (!empty($cur_field['js']))
753
				$context['profile_javascript'] .= $cur_field['js'];
754
755
			// Any template stuff?
756
			if (!empty($cur_field['prehtml']))
757
				$context['profile_prehtml'] .= $cur_field['prehtml'];
758
			if (!empty($cur_field['posthtml']))
759
				$context['profile_posthtml'] .= $cur_field['posthtml'];
760
761
			// Finally put it into context?
762
			if ($cur_field['type'] != 'hidden')
763
			{
764
				$last_type = $cur_field['type'];
765
				$context['profile_fields'][$field] = &$profile_fields[$field];
766
			}
767
		}
768
		// Bodge in a line break - without doing two in a row ;)
769
		elseif ($field == 'hr' && $last_type != 'hr' && $last_type != '')
770
		{
771
			$last_type = 'hr';
772
			$context['profile_fields'][$i++]['type'] = 'hr';
773
		}
774
	}
775
776
	// Some spicy JS.
777
	addInlineJavaScript('
778
	var form_handle = document.forms.creator;
779
	createEventListener(form_handle);
780
	' . (!empty($context['require_password']) ? '
781
	form_handle.addEventListener("submit", function(event)
782
	{
783
		if (this.oldpasswrd.value == "")
784
		{
785
			event.preventDefault();
786
			alert(' . (JavaScriptEscape($txt['required_security_reasons'])) . ');
787
			return false;
788
		}
789
	}, false);' : ''), true);
790
791
	// Any onsubmit javascript?
792
	if (!empty($context['profile_onsubmit_javascript']))
793
		addInlineJavaScript($context['profile_onsubmit_javascript'], true);
794
795
	// Any totally custom stuff?
796
	if (!empty($context['profile_javascript']))
797
		addInlineJavaScript($context['profile_javascript'], true);
798
799
	// Free up some memory.
800
	unset($profile_fields);
801
}
802
803
/**
804
 * Save the profile changes.
805
 */
806
function saveProfileFields()
807
{
808
	global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile, $smcFunc;
809
810
	// Load them up.
811
	loadProfileFields();
812
813
	// This makes things easier...
814
	$old_profile = $cur_profile;
815
816
	// This allows variables to call activities when they save - by default just to reload their settings
817
	$context['profile_execute_on_save'] = array();
818
	if ($context['user']['is_owner'])
819
		$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
820
821
	// Assume we log nothing.
822
	$context['log_changes'] = array();
823
824
	// Cycle through the profile fields working out what to do!
825
	foreach ($profile_fields as $key => $field)
826
	{
827
		if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key == 'signature'))
828
			continue;
829
830
		$_POST[$key] = sanitize_chars($smcFunc['normalize']($_POST[$key]), in_array($key, array('member_name', 'real_name')) ? 1 : 0);
831
832
		// What gets updated?
833
		$db_key = isset($field['save_key']) ? $field['save_key'] : $key;
834
835
		// Right - we have something that is enabled, we can act upon and has a value posted to it. Does it have a validation function?
836
		if (isset($field['input_validate']))
837
		{
838
			$is_valid = $field['input_validate']($_POST[$key]);
839
			// An error occurred - set it as such!
840
			if ($is_valid !== true)
841
			{
842
				// Is this an actual error?
843
				if ($is_valid !== false)
844
				{
845
					$post_errors[$key] = $is_valid;
846
					$profile_fields[$key]['is_error'] = $is_valid;
847
				}
848
				// Retain the old value.
849
				$cur_profile[$key] = $_POST[$key];
850
				continue;
851
			}
852
		}
853
854
		// Are we doing a cast?
855
		$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
856
857
		// Finally, clean up certain types.
858
		if ($field['cast_type'] == 'int')
859
			$_POST[$key] = (int) $_POST[$key];
860
		elseif ($field['cast_type'] == 'float')
861
			$_POST[$key] = (float) $_POST[$key];
862
		elseif ($field['cast_type'] == 'check')
863
			$_POST[$key] = !empty($_POST[$key]) ? 1 : 0;
864
865
		// If we got here we're doing OK.
866
		if ($field['type'] != 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
867
		{
868
			// Set the save variable.
869
			$profile_vars[$db_key] = $_POST[$key];
870
			// And update the user profile.
871
			$cur_profile[$key] = $_POST[$key];
872
873
			// Are we logging it?
874
			if (!empty($field['log_change']) && isset($old_profile[$key]))
875
				$context['log_changes'][$key] = array(
876
					'previous' => $old_profile[$key],
877
					'new' => $_POST[$key],
878
				);
879
		}
880
881
		// Logging group changes are a bit different...
882
		if ($key == 'id_group' && $field['log_change'])
883
		{
884
			profileLoadGroups();
885
886
			// Any changes to primary group?
887
			if ($_POST['id_group'] != $old_profile['id_group'])
888
			{
889
				$context['log_changes']['id_group'] = array(
890
					'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
891
					'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
892
				);
893
			}
894
895
			// Prepare additional groups for comparison.
896
			$additional_groups = array(
897
				'previous' => !empty($old_profile['additional_groups']) ? explode(',', $old_profile['additional_groups']) : array(),
898
				'new' => !empty($_POST['additional_groups']) ? array_diff($_POST['additional_groups'], array(0)) : array(),
899
			);
900
901
			sort($additional_groups['previous']);
902
			sort($additional_groups['new']);
903
904
			// What about additional groups?
905
			if ($additional_groups['previous'] != $additional_groups['new'])
906
			{
907
				foreach ($additional_groups as $type => $groups)
908
				{
909
					foreach ($groups as $id => $group)
910
					{
911
						if (isset($context['member_groups'][$group]))
912
							$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
913
						else
914
							unset($additional_groups[$type][$id]);
915
					}
916
					$additional_groups[$type] = implode(', ', $additional_groups[$type]);
917
				}
918
919
				$context['log_changes']['additional_groups'] = $additional_groups;
920
			}
921
		}
922
	}
923
924
	// @todo Temporary
925
	if ($context['user']['is_owner'])
926
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
927
	else
928
		$changeOther = allowedTo('profile_extra_any');
929
	if ($changeOther && empty($post_errors))
930
	{
931
		makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
932
		if (!empty($_REQUEST['sa']))
933
		{
934
			$custom_fields_errors = makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false, true);
935
936
			if (!empty($custom_fields_errors))
937
				$post_errors = array_merge($post_errors, $custom_fields_errors);
938
		}
939
	}
940
941
	// Free memory!
942
	unset($profile_fields);
943
}
944
945
/**
946
 * Save the profile changes
947
 *
948
 * @param array &$profile_vars The items to save
949
 * @param array &$post_errors An array of information about any errors that occurred
950
 * @param int $memID The ID of the member whose profile we're saving
951
 */
952
function saveProfileChanges(&$profile_vars, &$post_errors, $memID)
953
{
954
	global $user_profile, $context;
955
956
	// These make life easier....
957
	$old_profile = &$user_profile[$memID];
958
959
	// Permissions...
960
	if ($context['user']['is_owner'])
961
	{
962
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own', 'profile_website_any', 'profile_website_own', 'profile_signature_any', 'profile_signature_own'));
963
	}
964
	else
965
		$changeOther = allowedTo(array('profile_extra_any', 'profile_website_any', 'profile_signature_any'));
966
967
	// Arrays of all the changes - makes things easier.
968
	$profile_bools = array();
969
	$profile_ints = array();
970
	$profile_floats = array();
971
	$profile_strings = array(
972
		'buddy_list',
973
		'ignore_boards',
974
	);
975
976
	if (isset($_POST['sa']) && $_POST['sa'] == 'ignoreboards' && empty($_POST['brd']))
977
		$_POST['brd'] = array();
978
979
	unset($_POST['ignore_boards']); // Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
980
	if (isset($_POST['brd']))
981
	{
982
		if (!is_array($_POST['brd']))
983
			$_POST['brd'] = array($_POST['brd']);
984
985
		foreach ($_POST['brd'] as $k => $d)
986
		{
987
			$d = (int) $d;
988
			if ($d != 0)
989
				$_POST['brd'][$k] = $d;
990
			else
991
				unset($_POST['brd'][$k]);
992
		}
993
		$_POST['ignore_boards'] = implode(',', $_POST['brd']);
994
		unset($_POST['brd']);
995
	}
996
997
	// Here's where we sort out all the 'other' values...
998
	if ($changeOther)
999
	{
1000
		makeThemeChanges($memID, isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
1001
		//makeAvatarChanges($memID, $post_errors);
1002
1003
		if (!empty($_REQUEST['sa']))
1004
			makeCustomFieldChanges($memID, $_REQUEST['sa'], false);
1005
1006
		foreach ($profile_bools as $var)
1007
			if (isset($_POST[$var]))
1008
				$profile_vars[$var] = empty($_POST[$var]) ? '0' : '1';
1009
		foreach ($profile_ints as $var)
1010
			if (isset($_POST[$var]))
1011
				$profile_vars[$var] = $_POST[$var] != '' ? (int) $_POST[$var] : '';
1012
		foreach ($profile_floats as $var)
1013
			if (isset($_POST[$var]))
1014
				$profile_vars[$var] = (float) $_POST[$var];
1015
		foreach ($profile_strings as $var)
1016
			if (isset($_POST[$var]))
1017
				$profile_vars[$var] = $_POST[$var];
1018
	}
1019
}
1020
1021
/**
1022
 * Make any theme changes that are sent with the profile.
1023
 *
1024
 * @param int $memID The ID of the user
1025
 * @param int $id_theme The ID of the theme
1026
 */
1027
function makeThemeChanges($memID, $id_theme)
1028
{
1029
	global $modSettings, $smcFunc, $context, $user_info;
1030
1031
	$reservedVars = array(
1032
		'actual_theme_url',
1033
		'actual_images_url',
1034
		'base_theme_dir',
1035
		'base_theme_url',
1036
		'default_images_url',
1037
		'default_theme_dir',
1038
		'default_theme_url',
1039
		'default_template',
1040
		'images_url',
1041
		'number_recent_posts',
1042
		'smiley_sets_default',
1043
		'theme_dir',
1044
		'theme_id',
1045
		'theme_layers',
1046
		'theme_templates',
1047
		'theme_url',
1048
	);
1049
1050
	// Can't change reserved vars.
1051
	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))
1052
		fatal_lang_error('no_access', false);
1053
1054
	// Don't allow any overriding of custom fields with default or non-default options.
1055
	$request = $smcFunc['db_query']('', '
1056
		SELECT col_name
1057
		FROM {db_prefix}custom_fields
1058
		WHERE active = {int:is_active}',
1059
		array(
1060
			'is_active' => 1,
1061
		)
1062
	);
1063
	$custom_fields = array();
1064
	while ($row = $smcFunc['db_fetch_assoc']($request))
1065
		$custom_fields[] = $row['col_name'];
1066
	$smcFunc['db_free_result']($request);
1067
1068
	// These are the theme changes...
1069
	$themeSetArray = array();
1070
	if (isset($_POST['options']) && is_array($_POST['options']))
1071
	{
1072
		foreach ($_POST['options'] as $opt => $val)
1073
		{
1074
			if (in_array($opt, $custom_fields))
1075
				continue;
1076
1077
			// These need to be controlled.
1078
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1079
				$val = max(0, min($val, 50));
1080
			// We don't set this per theme anymore.
1081
			elseif ($opt == 'allow_no_censored')
1082
				continue;
1083
1084
			$themeSetArray[] = array($memID, $id_theme, $opt, is_array($val) ? implode(',', $val) : $val);
1085
		}
1086
	}
1087
1088
	$erase_options = array();
1089
	if (isset($_POST['default_options']) && is_array($_POST['default_options']))
1090
		foreach ($_POST['default_options'] as $opt => $val)
1091
		{
1092
			if (in_array($opt, $custom_fields))
1093
				continue;
1094
1095
			// These need to be controlled.
1096
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1097
				$val = max(0, min($val, 50));
1098
			// Only let admins and owners change the censor.
1099
			elseif ($opt == 'allow_no_censored' && !$user_info['is_admin'] && !$context['user']['is_owner'])
1100
				continue;
1101
1102
			$themeSetArray[] = array($memID, 1, $opt, is_array($val) ? implode(',', $val) : $val);
1103
			$erase_options[] = $opt;
1104
		}
1105
1106
	// If themeSetArray isn't still empty, send it to the database.
1107
	if (empty($context['password_auth_failed']))
1108
	{
1109
		if (!empty($themeSetArray))
1110
		{
1111
			$smcFunc['db_insert']('replace',
1112
				'{db_prefix}themes',
1113
				array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
1114
				$themeSetArray,
1115
				array('id_member', 'id_theme', 'variable')
1116
			);
1117
		}
1118
1119
		if (!empty($erase_options))
1120
		{
1121
			$smcFunc['db_query']('', '
1122
				DELETE FROM {db_prefix}themes
1123
				WHERE id_theme != {int:id_theme}
1124
					AND variable IN ({array_string:erase_variables})
1125
					AND id_member = {int:id_member}',
1126
				array(
1127
					'id_theme' => 1,
1128
					'id_member' => $memID,
1129
					'erase_variables' => $erase_options
1130
				)
1131
			);
1132
		}
1133
1134
		// Admins can choose any theme, even if it's not enabled...
1135
		$themes = allowedTo('admin_forum') ? explode(',', $modSettings['knownThemes']) : explode(',', $modSettings['enableThemes']);
1136
		foreach ($themes as $t)
1137
			cache_put_data('theme_settings-' . $t . ':' . $memID, null, 60);
1138
	}
1139
}
1140
1141
/**
1142
 * Make any notification changes that need to be made.
1143
 *
1144
 * @param int $memID The ID of the member
1145
 */
1146
function makeNotificationChanges($memID)
1147
{
1148
	global $smcFunc, $sourcedir;
1149
1150
	require_once($sourcedir . '/Subs-Notify.php');
1151
1152
	// Update the boards they are being notified on.
1153
	if (isset($_POST['edit_notify_boards']) && !empty($_POST['notify_boards']))
1154
	{
1155
		// Make sure only integers are deleted.
1156
		foreach ($_POST['notify_boards'] as $index => $id)
1157
			$_POST['notify_boards'][$index] = (int) $id;
1158
1159
		// id_board = 0 is reserved for topic notifications.
1160
		$_POST['notify_boards'] = array_diff($_POST['notify_boards'], array(0));
1161
1162
		$smcFunc['db_query']('', '
1163
			DELETE FROM {db_prefix}log_notify
1164
			WHERE id_board IN ({array_int:board_list})
1165
				AND id_member = {int:selected_member}',
1166
			array(
1167
				'board_list' => $_POST['notify_boards'],
1168
				'selected_member' => $memID,
1169
			)
1170
		);
1171
	}
1172
1173
	// We are editing topic notifications......
1174
	elseif (isset($_POST['edit_notify_topics']) && !empty($_POST['notify_topics']))
1175
	{
1176
		foreach ($_POST['notify_topics'] as $index => $id)
1177
			$_POST['notify_topics'][$index] = (int) $id;
1178
1179
		// Make sure there are no zeros left.
1180
		$_POST['notify_topics'] = array_diff($_POST['notify_topics'], array(0));
1181
1182
		$smcFunc['db_query']('', '
1183
			DELETE FROM {db_prefix}log_notify
1184
			WHERE id_topic IN ({array_int:topic_list})
1185
				AND id_member = {int:selected_member}',
1186
			array(
1187
				'topic_list' => $_POST['notify_topics'],
1188
				'selected_member' => $memID,
1189
			)
1190
		);
1191
		foreach ($_POST['notify_topics'] as $topic)
1192
			setNotifyPrefs((int) $memID, array('topic_notify_' . $topic => 0));
1193
	}
1194
1195
	// We are removing topic preferences
1196
	elseif (isset($_POST['remove_notify_topics']) && !empty($_POST['notify_topics']))
1197
	{
1198
		$prefs = array();
1199
		foreach ($_POST['notify_topics'] as $topic)
1200
			$prefs[] = 'topic_notify_' . $topic;
1201
		deleteNotifyPrefs($memID, $prefs);
1202
	}
1203
1204
	// We are removing board preferences
1205
	elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
1206
	{
1207
		$prefs = array();
1208
		foreach ($_POST['notify_boards'] as $board)
1209
			$prefs[] = 'board_notify_' . $board;
1210
		deleteNotifyPrefs($memID, $prefs);
1211
	}
1212
}
1213
1214
/**
1215
 * Save any changes to the custom profile fields
1216
 *
1217
 * @param int $memID The ID of the member
1218
 * @param string $area The area of the profile these fields are in
1219
 * @param bool $sanitize = true Whether or not to sanitize the data
1220
 * @param bool $returnErrors Whether or not to return any error information
1221
 * @return void|array Returns nothing or returns an array of error info if $returnErrors is true
1222
 */
1223
function makeCustomFieldChanges($memID, $area, $sanitize = true, $returnErrors = false)
1224
{
1225
	global $context, $smcFunc, $user_profile, $user_info, $modSettings;
1226
	global $sourcedir;
1227
1228
	$errors = array();
1229
1230
	if ($sanitize && isset($_POST['customfield']))
1231
		$_POST['customfield'] = htmlspecialchars__recursive($_POST['customfield']);
1232
1233
	$where = $area == 'register' ? 'show_reg != 0' : 'show_profile = {string:area}';
1234
1235
	// Load the fields we are saving too - make sure we save valid data (etc).
1236
	$request = $smcFunc['db_query']('', '
1237
		SELECT col_name, field_name, field_desc, field_type, field_length, field_options, default_value, show_reg, mask, private
1238
		FROM {db_prefix}custom_fields
1239
		WHERE ' . $where . '
1240
			AND active = {int:is_active}',
1241
		array(
1242
			'is_active' => 1,
1243
			'area' => $area,
1244
		)
1245
	);
1246
	$changes = array();
1247
	$deletes = array();
1248
	$log_changes = array();
1249
	while ($row = $smcFunc['db_fetch_assoc']($request))
1250
	{
1251
		/* This means don't save if:
1252
			- The user is NOT an admin.
1253
			- The data is not freely viewable and editable by users.
1254
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
1255
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
1256
		*/
1257
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != $user_info['id'] || $row['private'] != 2) && ($area != 'register' || $row['show_reg'] == 0))
1258
			continue;
1259
1260
		// Validate the user data.
1261
		if ($row['field_type'] == 'check')
1262
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
1263
		elseif ($row['field_type'] == 'select' || $row['field_type'] == 'radio')
1264
		{
1265
			$value = $row['default_value'];
1266
			foreach (explode(',', $row['field_options']) as $k => $v)
1267
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
1268
					$value = $v;
1269
		}
1270
		// Otherwise some form of text!
1271
		else
1272
		{
1273
			$value = isset($_POST['customfield'][$row['col_name']]) ? $_POST['customfield'][$row['col_name']] : '';
1274
1275
			if ($row['field_length'])
1276
				$value = $smcFunc['substr']($value, 0, $row['field_length']);
1277
1278
			// Any masks?
1279
			if ($row['field_type'] == 'text' && !empty($row['mask']) && $row['mask'] != 'none')
1280
			{
1281
				$value = $smcFunc['htmltrim']($value);
1282
				$valueReference = un_htmlspecialchars($value);
1283
1284
				// Try and avoid some checks. '0' could be a valid non-empty value.
1285
				if (empty($value) && !is_numeric($value))
1286
					$value = '';
1287
1288
				if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $value != $smcFunc['htmlspecialchars']($value, ENT_NOQUOTES) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
1289
				{
1290
					if ($returnErrors)
1291
						$errors[] = 'custom_field_nohtml_fail';
1292
1293
					else
1294
						$value = '';
1295
				}
1296
				elseif ($row['mask'] == 'email' && !empty($value) && (!filter_var($value, FILTER_VALIDATE_EMAIL) || strlen($value) > 255))
1297
				{
1298
					if ($returnErrors)
1299
						$errors[] = 'custom_field_mail_fail';
1300
1301
					else
1302
						$value = '';
1303
				}
1304
				elseif ($row['mask'] == 'number')
1305
				{
1306
					$value = (int) $value;
1307
				}
1308
				elseif (substr($row['mask'], 0, 5) == 'regex' && trim($value) != '' && preg_match(substr($row['mask'], 5), $value) === 0)
1309
				{
1310
					if ($returnErrors)
1311
						$errors[] = 'custom_field_regex_fail';
1312
1313
					else
1314
						$value = '';
1315
				}
1316
1317
				unset($valueReference);
1318
			}
1319
		}
1320
1321
		if (!isset($user_profile[$memID]['options'][$row['col_name']]))
1322
			$user_profile[$memID]['options'][$row['col_name']] = '';
1323
1324
		// Did it change?
1325
		if ($user_profile[$memID]['options'][$row['col_name']] != $value)
1326
		{
1327
			$log_changes[] = array(
1328
				'action' => 'customfield_' . $row['col_name'],
1329
				'log_type' => 'user',
1330
				'extra' => array(
1331
					'previous' => !empty($user_profile[$memID]['options'][$row['col_name']])
1332
						? $user_profile[$memID]['options'][$row['col_name']]
1333
						: '',
1334
					'new' => $value,
1335
					// The applicator is the same as the member affected
1336
					// if we are registering a new member.
1337
					'applicator' => empty($user_info['id']) && $area == 'register'
1338
						? $memID
1339
						: $user_info['id'],
1340
					'member_affected' => $memID,
1341
				),
1342
			);
1343
			if (empty($value))
1344
			{
1345
				$deletes[] = array('id_theme' => 1, 'variable' => $row['col_name'], 'id_member' => $memID);
1346
				unset($user_profile[$memID]['options'][$row['col_name']]);
1347
			}
1348
			else
1349
			{
1350
				$changes[] = array(1, $row['col_name'], $value, $memID);
1351
				$user_profile[$memID]['options'][$row['col_name']] = $value;
1352
			}
1353
		}
1354
	}
1355
	$smcFunc['db_free_result']($request);
1356
1357
	$hook_errors = call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, &$errors, $returnErrors, $memID, $area, $sanitize, &$deletes));
1358
1359
	if (!empty($hook_errors) && is_array($hook_errors))
1360
		$errors = array_merge($errors, $hook_errors);
1361
1362
	// Make those changes!
1363
	if ((!empty($changes) || !empty($deletes)) && empty($context['password_auth_failed']) && empty($errors))
1364
	{
1365
		if (!empty($changes))
1366
			$smcFunc['db_insert']('replace',
1367
				'{db_prefix}themes',
1368
				array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
1369
				$changes,
1370
				array('id_theme', 'variable', 'id_member')
1371
			);
1372
		if (!empty($deletes))
1373
			foreach ($deletes as $delete)
1374
				$smcFunc['db_query']('', '
1375
					DELETE FROM {db_prefix}themes
1376
					WHERE id_theme = {int:id_theme}
1377
						AND variable = {string:variable}
1378
						AND id_member = {int:id_member}',
1379
					$delete
1380
				);
1381
		if (!empty($log_changes) && !empty($modSettings['modlog_enabled']))
1382
		{
1383
			require_once($sourcedir . '/Logging.php');
1384
			logActions($log_changes);
1385
		}
1386
	}
1387
1388
	if ($returnErrors)
1389
		return $errors;
1390
}
1391
1392
/**
1393
 * Show all the users buddies, as well as a add/delete interface.
1394
 *
1395
 * @param int $memID The ID of the member
1396
 */
1397
function editBuddyIgnoreLists($memID)
1398
{
1399
	global $context, $txt, $modSettings;
1400
1401
	// Do a quick check to ensure people aren't getting here illegally!
1402
	if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
1403
		fatal_lang_error('no_access', false);
1404
1405
	// Can we email the user direct?
1406
	$context['can_moderate_forum'] = allowedTo('moderate_forum');
1407
	$context['can_send_email'] = allowedTo('moderate_forum');
1408
1409
	$subActions = array(
1410
		'buddies' => array('editBuddies', $txt['editBuddies']),
1411
		'ignore' => array('editIgnoreList', $txt['editIgnoreList']),
1412
	);
1413
1414
	$context['list_area'] = isset($_GET['sa']) && isset($subActions[$_GET['sa']]) ? $_GET['sa'] : 'buddies';
1415
1416
	// Create the tabs for the template.
1417
	$context[$context['profile_menu_name']]['tab_data'] = array(
1418
		'title' => $txt['editBuddyIgnoreLists'],
1419
		'description' => $txt['buddy_ignore_desc'],
1420
		'icon_class' => 'main_icons profile_hd',
1421
		'tabs' => array(
1422
			'buddies' => array(),
1423
			'ignore' => array(),
1424
		),
1425
	);
1426
1427
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
1428
1429
	// Pass on to the actual function.
1430
	$context['sub_template'] = $subActions[$context['list_area']][0];
1431
	$call = call_helper($subActions[$context['list_area']][0], true);
1432
1433
	if (!empty($call))
1434
		call_user_func($call, $memID);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback 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

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

2355
		$count = alert_count(/** @scrutinizer ignore-type */ $memID, true);
Loading history...
2356
2357
		updateMemberData($memID, array('alerts' => $count));
2358
2359
		// Might want to know this.
2360
		return $count;
2361
	}
2362
}
2363
2364
/**
2365
 * Deletes all the alerts that a user has already read.
2366
 *
2367
 * @param int $memID The user ID. Defaults to the current user's ID.
2368
 */
2369
function alert_purge($memID = 0)
2370
{
2371
	global $smcFunc, $user_info;
2372
2373
	$memID = !empty($memID) && is_int($memID) ? $memID : $user_info['id'];
2374
2375
	$smcFunc['db_query']('', '
2376
		DELETE FROM {db_prefix}user_alerts
2377
		WHERE id_member = {int:memID}
2378
			AND is_read > 0',
2379
		array(
2380
			'memID' => $memID,
2381
		)
2382
	);
2383
}
2384
2385
/**
2386
 * Counts how many alerts a user has - either unread or all depending on $unread
2387
 * We can't use db_num_rows here, as we have to determine what boards the user can see
2388
 * Possibly in future versions as database support for json is mainstream, we can simplify this.
2389
 *
2390
 * @param int $memID The user ID.
2391
 * @param bool $unread Whether to only count unread alerts.
2392
 * @return int The number of requested alerts
2393
 */
2394
function alert_count($memID, $unread = false)
2395
{
2396
	global $smcFunc, $user_info;
2397
2398
	if (empty($memID))
2399
		return false;
2400
2401
	$alerts = array();
2402
	$possible_topics = array();
2403
	$possible_msgs = array();
2404
	$possible_attachments = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $possible_attachments is dead and can be removed.
Loading history...
2405
2406
	// We have to do this the slow way as to iterate over all possible boards the user can see.
2407
	$request = $smcFunc['db_query']('', '
2408
		SELECT id_alert, content_id, content_type, content_action, is_read
2409
		FROM {db_prefix}user_alerts
2410
		WHERE id_member = {int:id_member}
2411
			' . ($unread ? '
2412
			AND is_read = 0' : ''),
2413
		array(
2414
			'id_member' => $memID,
2415
		)
2416
	);
2417
	// First we dump alerts and possible boards information out.
2418
	while ($row = $smcFunc['db_fetch_assoc']($request))
2419
	{
2420
		$alerts[$row['id_alert']] = $row;
2421
2422
		// For these types, we need to check whether they can actually see the content.
2423
		if ($row['content_type'] == 'msg')
2424
		{
2425
			$alerts[$row['id_alert']]['visible'] = false;
2426
			$possible_msgs[$row['id_alert']] = $row['content_id'];
2427
		}
2428
		elseif (in_array($row['content_type'], array('topic', 'board')))
2429
		{
2430
			$alerts[$row['id_alert']]['visible'] = false;
2431
			$possible_topics[$row['id_alert']] = $row['content_id'];
2432
		}
2433
		// For the rest, they can always see it.
2434
		else
2435
			$alerts[$row['id_alert']]['visible'] = true;
2436
	}
2437
	$smcFunc['db_free_result']($request);
2438
2439
	// If we need to check board access, use the correct board access filter for the member in question.
2440
	if ((!isset($user_info['query_see_board']) || $user_info['id'] != $memID) && (!empty($possible_msgs) || !empty($possible_topics)))
2441
		$qb = build_query_board($memID);
2442
	else
2443
	{
2444
		$qb['query_see_topic_board'] = '{query_see_topic_board}';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$qb was never initialized. Although not strictly required by PHP, it is generally a good practice to add $qb = array(); before regardless.
Loading history...
2445
		$qb['query_see_message_board'] = '{query_see_message_board}';
2446
	}
2447
2448
	// We want only the stuff they can see.
2449
	if (!empty($possible_msgs))
2450
	{
2451
		$flipped_msgs = array();
2452
		foreach ($possible_msgs as $id_alert => $id_msg)
2453
		{
2454
			if (!isset($flipped_msgs[$id_msg]))
2455
				$flipped_msgs[$id_msg] = array();
2456
2457
			$flipped_msgs[$id_msg][] = $id_alert;
2458
		}
2459
2460
		$request = $smcFunc['db_query']('', '
2461
			SELECT m.id_msg
2462
			FROM {db_prefix}messages AS m
2463
			WHERE ' . $qb['query_see_message_board'] . '
2464
				AND m.id_msg IN ({array_int:msgs})',
2465
			array(
2466
				'msgs' => $possible_msgs,
2467
			)
2468
		);
2469
		while ($row = $smcFunc['db_fetch_assoc']($request))
2470
		{
2471
			foreach ($flipped_msgs[$row['id_msg']] as $id_alert)
2472
				$alerts[$id_alert]['visible'] = true;
2473
		}
2474
		$smcFunc['db_free_result']($request);
2475
	}
2476
	if (!empty($possible_topics))
2477
	{
2478
		$flipped_topics = array();
2479
		foreach ($possible_topics as $id_alert => $id_topic)
2480
		{
2481
			if (!isset($flipped_topics[$id_topic]))
2482
				$flipped_topics[$id_topic] = array();
2483
2484
			$flipped_topics[$id_topic][] = $id_alert;
2485
		}
2486
2487
		$request = $smcFunc['db_query']('', '
2488
			SELECT t.id_topic
2489
			FROM {db_prefix}topics AS t
2490
			WHERE ' . $qb['query_see_topic_board'] . '
2491
				AND t.id_topic IN ({array_int:topics})',
2492
			array(
2493
				'topics' => $possible_topics,
2494
			)
2495
		);
2496
		while ($row = $smcFunc['db_fetch_assoc']($request))
2497
		{
2498
			foreach ($flipped_topics[$row['id_topic']] as $id_alert)
2499
				$alerts[$id_alert]['visible'] = true;
2500
		}
2501
		$smcFunc['db_free_result']($request);
2502
	}
2503
2504
	// Now check alerts again and remove any they can't see.
2505
	$deletes = array();
2506
	$num_unread_deletes = 0;
2507
	foreach ($alerts as $id_alert => $alert)
2508
	{
2509
		if (!$alert['visible'])
2510
		{
2511
			if (empty($alert['is_read']))
2512
				$num_unread_deletes++;
2513
2514
			unset($alerts[$id_alert]);
2515
			$deletes[] = $id_alert;
2516
		}
2517
	}
2518
2519
	// Penultimate task - delete these orphaned, invisible alerts, otherwise they might hang around forever.
2520
	// This can happen if they are deleted or moved to a board this user cannot access.
2521
	// Note that unread alerts are never purged.
2522
	if (!empty($deletes))
2523
	{
2524
		$smcFunc['db_query']('', '
2525
			DELETE FROM {db_prefix}user_alerts
2526
			WHERE id_alert IN ({array_int:alerts})',
2527
			array(
2528
				'alerts' => $deletes,
2529
			)
2530
		);
2531
	}
2532
2533
	// One last thing - tweak counter on member record.
2534
	// Do it directly, as updateMemberData() calls this function, and may create a loop.
2535
	// Note that $user_info is not populated when this is invoked via cron, hence the CASE statement.
2536
	if ($num_unread_deletes > 0)
2537
	{
2538
		$smcFunc['db_query']('', '
2539
			UPDATE {db_prefix}members
2540
			SET alerts =
2541
				CASE
2542
					WHEN alerts < {int:unread_deletes} THEN 0
2543
					ELSE alerts - {int:unread_deletes}
2544
				END
2545
			WHERE id_member = {int:member}',
2546
			array(
2547
				'unread_deletes' => $num_unread_deletes,
2548
				'member' => $memID,
2549
			)
2550
		);
2551
	}
2552
2553
	return count($alerts);
2554
}
2555
2556
/**
2557
 * Handles alerts related to topics and posts
2558
 *
2559
 * @param int $memID The ID of the member
2560
 */
2561
function alert_notifications_topics($memID)
2562
{
2563
	global $txt, $scripturl, $context, $modSettings, $sourcedir;
2564
2565
	// Because of the way this stuff works, we want to do this ourselves.
2566
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
2567
	{
2568
		checkSession();
2569
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2570
2571
		makeNotificationChanges($memID);
2572
		$context['profile_updated'] = $txt['profile_updated_own'];
2573
	}
2574
2575
	// Now set up for the token check.
2576
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2577
	createToken($context['token_check'], 'post');
2578
2579
	// Gonna want this for the list.
2580
	require_once($sourcedir . '/Subs-List.php');
2581
2582
	// Do the topic notifications.
2583
	$listOptions = array(
2584
		'id' => 'topic_notification_list',
2585
		'width' => '100%',
2586
		'items_per_page' => $modSettings['defaultMaxListItems'],
2587
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2588
		'no_items_align' => 'left',
2589
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2590
		'default_sort_col' => 'last_post',
2591
		'get_items' => array(
2592
			'function' => 'list_getTopicNotifications',
2593
			'params' => array(
2594
				$memID,
2595
			),
2596
		),
2597
		'get_count' => array(
2598
			'function' => 'list_getTopicNotificationCount',
2599
			'params' => array(
2600
				$memID,
2601
			),
2602
		),
2603
		'columns' => array(
2604
			'subject' => array(
2605
				'header' => array(
2606
					'value' => $txt['notifications_topics'],
2607
					'class' => 'lefttext',
2608
				),
2609
				'data' => array(
2610
					'function' => function($topic) use ($txt)
2611
					{
2612
						$link = $topic['link'];
2613
2614
						if ($topic['new'])
2615
							$link .= ' <a href="' . $topic['new_href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2616
2617
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2618
2619
						return $link;
2620
					},
2621
				),
2622
				'sort' => array(
2623
					'default' => 'ms.subject',
2624
					'reverse' => 'ms.subject DESC',
2625
				),
2626
			),
2627
			'started_by' => array(
2628
				'header' => array(
2629
					'value' => $txt['started_by'],
2630
					'class' => 'lefttext',
2631
				),
2632
				'data' => array(
2633
					'db' => 'poster_link',
2634
				),
2635
				'sort' => array(
2636
					'default' => 'real_name_col',
2637
					'reverse' => 'real_name_col DESC',
2638
				),
2639
			),
2640
			'last_post' => array(
2641
				'header' => array(
2642
					'value' => $txt['last_post'],
2643
					'class' => 'lefttext',
2644
				),
2645
				'data' => array(
2646
					'sprintf' => array(
2647
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2648
						'params' => array(
2649
							'updated' => false,
2650
							'poster_updated_link' => false,
2651
						),
2652
					),
2653
				),
2654
				'sort' => array(
2655
					'default' => 'ml.id_msg DESC',
2656
					'reverse' => 'ml.id_msg',
2657
				),
2658
			),
2659
			'alert_pref' => array(
2660
				'header' => array(
2661
					'value' => $txt['notify_what_how'],
2662
					'class' => 'lefttext',
2663
				),
2664
				'data' => array(
2665
					'function' => function($topic) use ($txt)
2666
					{
2667
						$pref = $topic['notify_pref'];
2668
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2669
						return $txt['notify_topic_' . $mode];
2670
					},
2671
				),
2672
			),
2673
			'delete' => array(
2674
				'header' => array(
2675
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2676
					'style' => 'width: 4%;',
2677
					'class' => 'centercol',
2678
				),
2679
				'data' => array(
2680
					'sprintf' => array(
2681
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d">',
2682
						'params' => array(
2683
							'id' => false,
2684
						),
2685
					),
2686
					'class' => 'centercol',
2687
				),
2688
			),
2689
		),
2690
		'form' => array(
2691
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2692
			'include_sort' => true,
2693
			'include_start' => true,
2694
			'hidden_fields' => array(
2695
				'u' => $memID,
2696
				'sa' => $context['menu_item_selected'],
2697
				$context['session_var'] => $context['session_id'],
2698
			),
2699
			'token' => $context['token_check'],
2700
		),
2701
		'additional_rows' => array(
2702
			array(
2703
				'position' => 'bottom_of_list',
2704
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button" />
2705
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2706
				'class' => 'floatright',
2707
			),
2708
		),
2709
	);
2710
2711
	// Create the notification list.
2712
	createList($listOptions);
2713
}
2714
2715
/**
2716
 * Handles preferences related to board-level notifications
2717
 *
2718
 * @param int $memID The ID of the member
2719
 */
2720
function alert_notifications_boards($memID)
2721
{
2722
	global $txt, $scripturl, $context, $sourcedir;
2723
2724
	// Because of the way this stuff works, we want to do this ourselves.
2725
	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...
2726
	{
2727
		checkSession();
2728
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2729
2730
		makeNotificationChanges($memID);
2731
		$context['profile_updated'] = $txt['profile_updated_own'];
2732
	}
2733
2734
	// Now set up for the token check.
2735
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2736
	createToken($context['token_check'], 'post');
2737
2738
	// Gonna want this for the list.
2739
	require_once($sourcedir . '/Subs-List.php');
2740
2741
	// Fine, start with the board list.
2742
	$listOptions = array(
2743
		'id' => 'board_notification_list',
2744
		'width' => '100%',
2745
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2746
		'no_items_align' => 'left',
2747
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2748
		'default_sort_col' => 'board_name',
2749
		'get_items' => array(
2750
			'function' => 'list_getBoardNotifications',
2751
			'params' => array(
2752
				$memID,
2753
			),
2754
		),
2755
		'columns' => array(
2756
			'board_name' => array(
2757
				'header' => array(
2758
					'value' => $txt['notifications_boards'],
2759
					'class' => 'lefttext',
2760
				),
2761
				'data' => array(
2762
					'function' => function($board) use ($txt)
2763
					{
2764
						$link = $board['link'];
2765
2766
						if ($board['new'])
2767
							$link .= ' <a href="' . $board['href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2768
2769
						return $link;
2770
					},
2771
				),
2772
				'sort' => array(
2773
					'default' => 'name',
2774
					'reverse' => 'name DESC',
2775
				),
2776
			),
2777
			'alert_pref' => array(
2778
				'header' => array(
2779
					'value' => $txt['notify_what_how'],
2780
					'class' => 'lefttext',
2781
				),
2782
				'data' => array(
2783
					'function' => function($board) use ($txt)
2784
					{
2785
						$pref = $board['notify_pref'];
2786
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2787
						return $txt['notify_board_' . $mode];
2788
					},
2789
				),
2790
			),
2791
			'delete' => array(
2792
				'header' => array(
2793
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2794
					'style' => 'width: 4%;',
2795
					'class' => 'centercol',
2796
				),
2797
				'data' => array(
2798
					'sprintf' => array(
2799
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d">',
2800
						'params' => array(
2801
							'id' => false,
2802
						),
2803
					),
2804
					'class' => 'centercol',
2805
				),
2806
			),
2807
		),
2808
		'form' => array(
2809
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2810
			'include_sort' => true,
2811
			'include_start' => true,
2812
			'hidden_fields' => array(
2813
				'u' => $memID,
2814
				'sa' => $context['menu_item_selected'],
2815
				$context['session_var'] => $context['session_id'],
2816
			),
2817
			'token' => $context['token_check'],
2818
		),
2819
		'additional_rows' => array(
2820
			array(
2821
				'position' => 'bottom_of_list',
2822
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button">
2823
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2824
				'class' => 'floatright',
2825
			),
2826
		),
2827
	);
2828
2829
	// Create the board notification list.
2830
	createList($listOptions);
2831
}
2832
2833
/**
2834
 * Determins how many topics a user has requested notifications for
2835
 *
2836
 * @param int $memID The ID of the member
2837
 * @return int The number of topic notifications for this user
2838
 */
2839
function list_getTopicNotificationCount($memID)
2840
{
2841
	global $smcFunc, $user_info, $modSettings;
2842
2843
	$request = $smcFunc['db_query']('', '
2844
		SELECT COUNT(*)
2845
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2846
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . '
2847
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_topic_board'] === '1=1' ? '' : '
2848
			AND {query_see_topic_board}') . ($modSettings['postmod_active'] ? '
2849
			AND t.approved = {int:is_approved}' : ''),
2850
		array(
2851
			'selected_member' => $memID,
2852
			'is_approved' => 1,
2853
		)
2854
	);
2855
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2856
	$smcFunc['db_free_result']($request);
2857
2858
	return (int) $totalNotifications;
2859
}
2860
2861
/**
2862
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2863
 *
2864
 * @param int $start Which item to start with (for pagination purposes)
2865
 * @param int $items_per_page How many items to display on each page
2866
 * @param string $sort A string indicating how to sort the results
2867
 * @param int $memID The ID of the member
2868
 * @return array An array of information about the topics a user has subscribed to
2869
 */
2870
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2871
{
2872
	global $smcFunc, $scripturl, $user_info, $modSettings, $sourcedir;
2873
2874
	require_once($sourcedir . '/Subs-Notify.php');
2875
	$prefs = getNotifyPrefs($memID);
2876
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2877
2878
	// All the topics with notification on...
2879
	$request = $smcFunc['db_query']('', '
2880
		SELECT
2881
			COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from, b.id_board, b.name,
2882
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2883
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2884
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2885
			lt.unwatched
2886
		FROM {db_prefix}log_notify AS ln
2887
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2888
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2889
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2890
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2891
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2892
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2893
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2894
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2895
		WHERE ln.id_member = {int:selected_member}
2896
		ORDER BY {raw:sort}
2897
		LIMIT {int:offset}, {int:items_per_page}',
2898
		array(
2899
			'current_member' => $user_info['id'],
2900
			'is_approved' => 1,
2901
			'selected_member' => $memID,
2902
			'sort' => $sort,
2903
			'offset' => $start,
2904
			'items_per_page' => $items_per_page,
2905
		)
2906
	);
2907
	$notification_topics = array();
2908
	while ($row = $smcFunc['db_fetch_assoc']($request))
2909
	{
2910
		censorText($row['subject']);
2911
2912
		$notification_topics[] = array(
2913
			'id' => $row['id_topic'],
2914
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2915
			'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>',
2916
			'subject' => $row['subject'],
2917
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2918
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2919
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2920
			'new_from' => $row['new_from'],
2921
			'updated' => timeformat($row['poster_time']),
2922
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2923
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2924
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2925
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2926
			'unwatched' => $row['unwatched'],
2927
		);
2928
	}
2929
	$smcFunc['db_free_result']($request);
2930
2931
	return $notification_topics;
2932
}
2933
2934
/**
2935
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2936
 *
2937
 * @param int $start Which item to start with (not used here)
2938
 * @param int $items_per_page How many items to show on each page (not used here)
2939
 * @param string $sort A string indicating how to sort the results
2940
 * @param int $memID The ID of the member
2941
 * @return array An array of information about all the boards a user is subscribed to
2942
 */
2943
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2944
{
2945
	global $smcFunc, $scripturl, $user_info, $sourcedir;
2946
2947
	require_once($sourcedir . '/Subs-Notify.php');
2948
	$prefs = getNotifyPrefs($memID);
2949
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2950
2951
	$request = $smcFunc['db_query']('', '
2952
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2953
		FROM {db_prefix}log_notify AS ln
2954
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2955
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2956
		WHERE ln.id_member = {int:selected_member}
2957
			AND {query_see_board}
2958
		ORDER BY {raw:sort}',
2959
		array(
2960
			'current_member' => $user_info['id'],
2961
			'selected_member' => $memID,
2962
			'sort' => $sort,
2963
		)
2964
	);
2965
	$notification_boards = array();
2966
	while ($row = $smcFunc['db_fetch_assoc']($request))
2967
		$notification_boards[] = array(
2968
			'id' => $row['id_board'],
2969
			'name' => $row['name'],
2970
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2971
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2972
			'new' => $row['board_read'] < $row['id_msg_updated'],
2973
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2974
		);
2975
	$smcFunc['db_free_result']($request);
2976
2977
	return $notification_boards;
2978
}
2979
2980
/**
2981
 * Loads the theme options for a user
2982
 *
2983
 * @param int $memID The ID of the member
2984
 * @param bool $defaultSettings If true, we are loading default options.
2985
 */
2986
function loadThemeOptions($memID, $defaultSettings = false)
2987
{
2988
	global $context, $options, $cur_profile, $smcFunc;
2989
2990
	if (isset($_POST['default_options']))
2991
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2992
2993
	if ($context['user']['is_owner'])
2994
	{
2995
		$context['member']['options'] = $options;
2996
		if (isset($_POST['options']) && is_array($_POST['options']))
2997
			foreach ($_POST['options'] as $k => $v)
2998
				$context['member']['options'][$k] = $v;
2999
	}
3000
	else
3001
	{
3002
		$request = $smcFunc['db_query']('', '
3003
			SELECT id_member, variable, value
3004
			FROM {db_prefix}themes
3005
			WHERE id_theme IN (1, {int:member_theme})
3006
				AND id_member IN (-1, {int:selected_member})',
3007
			array(
3008
				'member_theme' => !isset($cur_profile['id_theme']) && !empty($defaultSettings) ? 0 : (int) $cur_profile['id_theme'],
3009
				'selected_member' => $memID,
3010
			)
3011
		);
3012
		$temp = array();
3013
		while ($row = $smcFunc['db_fetch_assoc']($request))
3014
		{
3015
			if ($row['id_member'] == -1)
3016
			{
3017
				$temp[$row['variable']] = $row['value'];
3018
				continue;
3019
			}
3020
3021
			if (isset($_POST['options'][$row['variable']]))
3022
				$row['value'] = $_POST['options'][$row['variable']];
3023
			$context['member']['options'][$row['variable']] = $row['value'];
3024
		}
3025
		$smcFunc['db_free_result']($request);
3026
3027
		// Load up the default theme options for any missing.
3028
		foreach ($temp as $k => $v)
3029
		{
3030
			if (!isset($context['member']['options'][$k]))
3031
				$context['member']['options'][$k] = $v;
3032
		}
3033
	}
3034
}
3035
3036
/**
3037
 * Handles the "ignored boards" section of the profile (if enabled)
3038
 *
3039
 * @param int $memID The ID of the member
3040
 */
3041
function ignoreboards($memID)
3042
{
3043
	global $context, $modSettings, $smcFunc, $cur_profile, $sourcedir;
3044
3045
	// Have the admins enabled this option?
3046
	if (empty($modSettings['allow_ignore_boards']))
3047
		fatal_lang_error('ignoreboards_disallowed', 'user');
3048
3049
	// Find all the boards this user is allowed to see.
3050
	$request = $smcFunc['db_query']('order_by_board_order', '
3051
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
3052
			' . (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
3053
		FROM {db_prefix}boards AS b
3054
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
3055
		WHERE {query_see_board}
3056
			AND redirect = {string:empty_string}',
3057
		array(
3058
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
3059
			'empty_string' => '',
3060
		)
3061
	);
3062
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
3063
	$context['categories'] = array();
3064
	while ($row = $smcFunc['db_fetch_assoc']($request))
3065
	{
3066
		// This category hasn't been set up yet..
3067
		if (!isset($context['categories'][$row['id_cat']]))
3068
			$context['categories'][$row['id_cat']] = array(
3069
				'id' => $row['id_cat'],
3070
				'name' => $row['cat_name'],
3071
				'boards' => array()
3072
			);
3073
3074
		// Set this board up, and let the template know when it's a child.  (indent them..)
3075
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
3076
			'id' => $row['id_board'],
3077
			'name' => $row['name'],
3078
			'child_level' => $row['child_level'],
3079
			'selected' => $row['is_ignored'],
3080
		);
3081
	}
3082
	$smcFunc['db_free_result']($request);
3083
3084
	require_once($sourcedir . '/Subs-Boards.php');
3085
	sortCategories($context['categories']);
3086
3087
	// Now, let's sort the list of categories into the boards for templates that like that.
3088
	$temp_boards = array();
3089
	foreach ($context['categories'] as $category)
3090
	{
3091
		// Include a list of boards per category for easy toggling.
3092
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
3093
3094
		$temp_boards[] = array(
3095
			'name' => $category['name'],
3096
			'child_ids' => array_keys($category['boards'])
3097
		);
3098
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
3099
	}
3100
3101
	$max_boards = ceil(count($temp_boards) / 2);
3102
	if ($max_boards == 1)
3103
		$max_boards = 2;
3104
3105
	// Now, alternate them so they can be shown left and right ;).
3106
	$context['board_columns'] = array();
3107
	for ($i = 0; $i < $max_boards; $i++)
3108
	{
3109
		$context['board_columns'][] = $temp_boards[$i];
3110
		if (isset($temp_boards[$i + $max_boards]))
3111
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
3112
		else
3113
			$context['board_columns'][] = array();
3114
	}
3115
3116
	loadThemeOptions($memID);
3117
}
3118
3119
/**
3120
 * Load all the languages for the profile
3121
 * .
3122
 * @return bool Whether or not the forum has multiple languages installed
3123
 */
3124
function profileLoadLanguages()
3125
{
3126
	global $context;
3127
3128
	$context['profile_languages'] = array();
3129
3130
	// Get our languages!
3131
	getLanguages();
3132
3133
	// Setup our languages.
3134
	foreach ($context['languages'] as $lang)
3135
	{
3136
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
3137
	}
3138
	ksort($context['profile_languages']);
3139
3140
	// Return whether we should proceed with this.
3141
	return count($context['profile_languages']) > 1 ? true : false;
3142
}
3143
3144
/**
3145
 * Handles the "manage groups" section of the profile
3146
 *
3147
 * @return true Always returns true
3148
 */
3149
function profileLoadGroups()
3150
{
3151
	global $cur_profile, $txt, $context, $smcFunc, $user_settings;
3152
3153
	$context['member_groups'] = array(
3154
		0 => array(
3155
			'id' => 0,
3156
			'name' => $txt['no_primary_membergroup'],
3157
			'is_primary' => $cur_profile['id_group'] == 0,
3158
			'can_be_additional' => false,
3159
			'can_be_primary' => true,
3160
		)
3161
	);
3162
	$curGroups = explode(',', $cur_profile['additional_groups']);
3163
3164
	// Load membergroups, but only those groups the user can assign.
3165
	$request = $smcFunc['db_query']('', '
3166
		SELECT group_name, id_group, hidden
3167
		FROM {db_prefix}membergroups
3168
		WHERE id_group != {int:moderator_group}
3169
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
3170
			AND group_type != {int:is_protected}') . '
3171
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
3172
		array(
3173
			'moderator_group' => 3,
3174
			'min_posts' => -1,
3175
			'is_protected' => 1,
3176
			'newbie_group' => 4,
3177
		)
3178
	);
3179
	while ($row = $smcFunc['db_fetch_assoc']($request))
3180
	{
3181
		// We should skip the administrator group if they don't have the admin_forum permission!
3182
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
3183
			continue;
3184
3185
		$context['member_groups'][$row['id_group']] = array(
3186
			'id' => $row['id_group'],
3187
			'name' => $row['group_name'],
3188
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
3189
			'is_additional' => in_array($row['id_group'], $curGroups),
3190
			'can_be_additional' => true,
3191
			'can_be_primary' => $row['hidden'] != 2,
3192
		);
3193
	}
3194
	$smcFunc['db_free_result']($request);
3195
3196
	$context['member']['group_id'] = $user_settings['id_group'];
3197
3198
	return true;
3199
}
3200
3201
/**
3202
 * Load key signature context data.
3203
 *
3204
 * @return true Always returns true
3205
 */
3206
function profileLoadSignatureData()
3207
{
3208
	global $modSettings, $context, $txt, $cur_profile, $memberContext, $smcFunc;
3209
3210
	// Signature limits.
3211
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3212
	$sig_limits = explode(',', $sig_limits);
3213
3214
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
3215
	$context['signature_limits'] = array(
3216
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
3217
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
3218
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
3219
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
3220
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
3221
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
3222
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
3223
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
3224
	);
3225
	// Kept this line in for backwards compatibility!
3226
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
3227
	// Warning message for signature image limits?
3228
	$context['signature_warning'] = '';
3229
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
3230
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
3231
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
3232
		$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']);
3233
3234
	if (empty($context['do_preview']))
3235
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<br/>', '<br />', '<', '>', '"', '\''), array("\n", "\n", "\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
3236
	else
3237
	{
3238
		$signature = $_POST['signature'] = !empty($_POST['signature']) ? $smcFunc['normalize']($_POST['signature']) : '';
3239
		$validation = profileValidateSignature($signature);
3240
		if (empty($context['post_errors']))
3241
		{
3242
			loadLanguage('Errors');
3243
			$context['post_errors'] = array();
3244
		}
3245
		$context['post_errors'][] = 'signature_not_yet_saved';
3246
		if ($validation !== true && $validation !== false)
3247
			$context['post_errors'][] = $validation;
3248
3249
		censorText($context['member']['signature']);
3250
		$context['member']['current_signature'] = $context['member']['signature'];
3251
		censorText($signature);
3252
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']], get_signature_allowed_bbc_tags());
3253
		$context['member']['signature'] = $_POST['signature'];
3254
	}
3255
3256
	// Load the spell checker?
3257
	if ($context['show_spellchecking'])
3258
		loadJavaScriptFile('spellcheck.js', array('defer' => false, 'minimize' => true), 'smf_spellcheck');
3259
3260
	return true;
3261
}
3262
3263
/**
3264
 * Load avatar context data.
3265
 *
3266
 * @return true Always returns true
3267
 */
3268
function profileLoadAvatarData()
3269
{
3270
	global $context, $cur_profile, $modSettings, $scripturl;
3271
3272
	$context['avatar_url'] = $modSettings['avatar_url'];
3273
3274
	// Default context.
3275
	$context['member']['avatar'] += array(
3276
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
3277
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
3278
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3279
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3280
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3281
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']),
3282
	);
3283
3284
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
3285
	{
3286
		$context['member']['avatar'] += array(
3287
			'choice' => 'gravatar',
3288
			'server_pic' => 'blank.png',
3289
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || (!empty($modSettings['gravatarOverride']) && substr($cur_profile['avatar'], 0, 11) != 'gravatar://') ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
3290
		);
3291
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
3292
	}
3293
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
3294
	{
3295
		$context['member']['avatar'] += array(
3296
			'choice' => 'upload',
3297
			'server_pic' => 'blank.png',
3298
			'external' => 'http://'
3299
		);
3300
		$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'];
3301
	}
3302
	// Use "avatar_original" here so we show what the user entered even if the image proxy is enabled
3303
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
3304
		$context['member']['avatar'] += array(
3305
			'choice' => 'external',
3306
			'server_pic' => 'blank.png',
3307
			'external' => $cur_profile['avatar_original']
3308
		);
3309
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
3310
		$context['member']['avatar'] += array(
3311
			'choice' => 'server_stored',
3312
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3313
			'external' => 'http://'
3314
		);
3315
	else
3316
		$context['member']['avatar'] += array(
3317
			'choice' => 'none',
3318
			'server_pic' => 'blank.png',
3319
			'external' => 'http://'
3320
		);
3321
3322
	// Get a list of all the avatars.
3323
	if ($context['member']['avatar']['allow_server_stored'])
3324
	{
3325
		$context['avatar_list'] = array();
3326
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3327
	}
3328
	else
3329
		$context['avatars'] = array();
3330
3331
	// Second level selected avatar...
3332
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3333
	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']);
3334
}
3335
3336
/**
3337
 * Save a members group.
3338
 *
3339
 * @param int &$value The ID of the (new) primary group
3340
 * @return true Always returns true
3341
 */
3342
function profileSaveGroups(&$value)
3343
{
3344
	global $profile_vars, $old_profile, $context, $smcFunc, $cur_profile;
3345
3346
	// Do we need to protect some groups?
3347
	if (!allowedTo('admin_forum'))
3348
	{
3349
		$request = $smcFunc['db_query']('', '
3350
			SELECT id_group
3351
			FROM {db_prefix}membergroups
3352
			WHERE group_type = {int:is_protected}',
3353
			array(
3354
				'is_protected' => 1,
3355
			)
3356
		);
3357
		$protected_groups = array(1);
3358
		while ($row = $smcFunc['db_fetch_assoc']($request))
3359
			$protected_groups[] = $row['id_group'];
3360
		$smcFunc['db_free_result']($request);
3361
3362
		$protected_groups = array_unique($protected_groups);
3363
	}
3364
3365
	// The account page allows the change of your id_group - but not to a protected group!
3366
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3367
		$value = (int) $value;
3368
	// ... otherwise it's the old group sir.
3369
	else
3370
		$value = $old_profile['id_group'];
3371
3372
	// Find the additional membergroups (if any)
3373
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3374
	{
3375
		$additional_groups = array();
3376
		foreach ($_POST['additional_groups'] as $group_id)
3377
		{
3378
			$group_id = (int) $group_id;
3379
			if (!empty($group_id) && (empty($protected_groups) || !in_array($group_id, $protected_groups)))
3380
				$additional_groups[] = $group_id;
3381
		}
3382
3383
		// Put the protected groups back in there if you don't have permission to take them away.
3384
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3385
		foreach ($old_additional_groups as $group_id)
3386
		{
3387
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3388
				$additional_groups[] = $group_id;
3389
		}
3390
3391
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3392
		{
3393
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3394
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3395
		}
3396
	}
3397
3398
	// Too often, people remove delete their own account, or something.
3399
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3400
	{
3401
		$stillAdmin = $value == 1 || (isset($additional_groups) && in_array(1, $additional_groups));
3402
3403
		// If they would no longer be an admin, look for any other...
3404
		if (!$stillAdmin)
3405
		{
3406
			$request = $smcFunc['db_query']('', '
3407
				SELECT id_member
3408
				FROM {db_prefix}members
3409
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3410
					AND id_member != {int:selected_member}
3411
				LIMIT 1',
3412
				array(
3413
					'admin_group' => 1,
3414
					'selected_member' => $context['id_member'],
3415
				)
3416
			);
3417
			list ($another) = $smcFunc['db_fetch_row']($request);
3418
			$smcFunc['db_free_result']($request);
3419
3420
			if (empty($another))
3421
				fatal_lang_error('at_least_one_admin', 'critical');
3422
		}
3423
	}
3424
3425
	// If we are changing group status, update permission cache as necessary.
3426
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3427
	{
3428
		if ($context['user']['is_owner'])
3429
			$_SESSION['mc']['time'] = 0;
3430
		else
3431
			updateSettings(array('settings_updated' => time()));
3432
	}
3433
3434
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3435
	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...
3436
3437
	return true;
3438
}
3439
3440
/**
3441
 * The avatar is incredibly complicated, what with the options... and what not.
3442
 *
3443
 * @todo argh, the avatar here. Take this out of here!
3444
 *
3445
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3446
 * @return bool|string False if success (or if memID is empty and password authentication failed), otherwise a string indicating what error occurred
3447
 */
3448
function profileSaveAvatarData(&$value)
3449
{
3450
	global $modSettings, $sourcedir, $smcFunc, $profile_vars, $cur_profile, $context;
3451
3452
	$memID = $context['id_member'];
3453
	$context['max_external_size_url'] = 255;
3454
3455
	if (empty($memID) && !empty($context['password_auth_failed']))
3456
		return false;
3457
3458
	require_once($sourcedir . '/ManageAttachments.php');
3459
3460
	call_integration_hook('before_profile_save_avatar', array(&$value));
3461
3462
	// External url too large
3463
	if ($value == 'external' && allowedTo('profile_remote_avatar') && strlen($_POST['userpicpersonal']) > $context['max_external_size_url'])
3464
		return 'bad_avatar_url_too_long';
3465
3466
	// We're going to put this on a nice custom dir.
3467
	$uploadDir = $modSettings['custom_avatar_dir'];
3468
	$id_folder = 1;
3469
3470
	$downloadedExternalAvatar = false;
3471
	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']))
3472
	{
3473
		if (!is_writable($uploadDir))
3474
			fatal_lang_error('attachments_no_write', 'critical');
3475
3476
		$url = parse_iri($_POST['userpicpersonal']);
3477
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3478
3479
		$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

3479
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, /** @scrutinizer ignore-type */ false, null, true);
Loading history...
3480
		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...
3481
		{
3482
			fwrite($tmpAvatar, $contents);
3483
			fclose($tmpAvatar);
3484
3485
			$downloadedExternalAvatar = true;
3486
			$_FILES['attachment']['tmp_name'] = $new_filename;
3487
		}
3488
	}
3489
3490
	// Removes whatever attachment there was before updating
3491
	if ($value == 'none')
3492
	{
3493
		$profile_vars['avatar'] = '';
3494
3495
		// Reset the attach ID.
3496
		$cur_profile['id_attach'] = 0;
3497
		$cur_profile['attachment_type'] = 0;
3498
		$cur_profile['filename'] = '';
3499
3500
		removeAttachments(array('id_member' => $memID));
3501
	}
3502
3503
	// An avatar from the server-stored galleries.
3504
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3505
	{
3506
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3507
		$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']) : '';
3508
3509
		// Clear current profile...
3510
		$cur_profile['id_attach'] = 0;
3511
		$cur_profile['attachment_type'] = 0;
3512
		$cur_profile['filename'] = '';
3513
3514
		// Get rid of their old avatar. (if uploaded.)
3515
		removeAttachments(array('id_member' => $memID));
3516
	}
3517
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3518
	{
3519
		// 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.
3520
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3521
			$profile_vars['avatar'] = 'gravatar://';
3522
		else
3523
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3524
3525
		// Get rid of their old avatar. (if uploaded.)
3526
		removeAttachments(array('id_member' => $memID));
3527
	}
3528
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3529
	{
3530
		// We need these clean...
3531
		$cur_profile['id_attach'] = 0;
3532
		$cur_profile['attachment_type'] = 0;
3533
		$cur_profile['filename'] = '';
3534
3535
		// Remove any attached avatar...
3536
		removeAttachments(array('id_member' => $memID));
3537
3538
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3539
		$mime_valid = check_mime_type($profile_vars['avatar'], 'image/', true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $is_path of check_mime_type(). ( Ignorable by Annotation )

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

3539
		$mime_valid = check_mime_type($profile_vars['avatar'], 'image/', /** @scrutinizer ignore-type */ true);
Loading history...
3540
3541
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3542
			$profile_vars['avatar'] = '';
3543
		// Trying to make us do something we'll regret?
3544
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3545
			return 'bad_avatar_invalid_url';
3546
		elseif (empty($mime_valid))
3547
			return 'bad_avatar';
3548
		// Should we check dimensions?
3549
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3550
		{
3551
			// Now let's validate the avatar.
3552
			$sizes = url_image_size($profile_vars['avatar']);
3553
3554
			if (is_array($sizes) && (($sizes[0] > $modSettings['avatar_max_width_external']
3555
				&& !empty($modSettings['avatar_max_width_external'])) || ($sizes[1] > $modSettings['avatar_max_height_external']
3556
				&& !empty($modSettings['avatar_max_height_external']))))
3557
			{
3558
				// Houston, we have a problem. The avatar is too large!!
3559
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3560
					return 'bad_avatar_too_large';
3561
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3562
				{
3563
					// @todo remove this if appropriate
3564
					require_once($sourcedir . '/Subs-Graphics.php');
3565
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3566
					{
3567
						$profile_vars['avatar'] = '';
3568
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3569
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3570
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3571
					}
3572
					else
3573
						return 'bad_avatar';
3574
				}
3575
			}
3576
		}
3577
	}
3578
3579
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3580
	{
3581
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3582
		{
3583
			// Get the dimensions of the image.
3584
			if (!$downloadedExternalAvatar)
3585
			{
3586
				if (!is_writable($uploadDir))
3587
					fatal_lang_error('avatars_no_write', 'critical');
3588
3589
				$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
3590
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3591
					fatal_lang_error('attach_timeout', 'critical');
3592
3593
				$_FILES['attachment']['tmp_name'] = $new_filename;
3594
			}
3595
3596
			$mime_valid = check_mime_type($_FILES['attachment']['tmp_name'], 'image/', true);
3597
			$sizes = empty($mime_valid) ? false : @getimagesize($_FILES['attachment']['tmp_name']);
3598
3599
			// No size, then it's probably not a valid pic.
3600
			if ($sizes === false)
3601
			{
3602
				@unlink($_FILES['attachment']['tmp_name']);
3603
				return 'bad_avatar';
3604
			}
3605
			// Check whether the image is too large.
3606
			elseif ((!empty($modSettings['avatar_max_width_upload']) && $sizes[0] > $modSettings['avatar_max_width_upload'])
3607
				|| (!empty($modSettings['avatar_max_height_upload']) && $sizes[1] > $modSettings['avatar_max_height_upload']))
3608
			{
3609
				if (!empty($modSettings['avatar_resize_upload']))
3610
				{
3611
					// Attempt to chmod it.
3612
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3613
3614
					// @todo remove this require when appropriate
3615
					require_once($sourcedir . '/Subs-Graphics.php');
3616
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3617
					{
3618
						@unlink($_FILES['attachment']['tmp_name']);
3619
						return 'bad_avatar';
3620
					}
3621
3622
					// Reset attachment avatar data.
3623
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3624
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3625
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3626
				}
3627
3628
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3629
				else
3630
				{
3631
					@unlink($_FILES['attachment']['tmp_name']);
3632
					return 'bad_avatar_too_large';
3633
				}
3634
			}
3635
3636
			// So far, so good, checks lies ahead!
3637
			elseif (is_array($sizes))
0 ignored issues
show
introduced by
The condition is_array($sizes) is always true.
Loading history...
3638
			{
3639
				// Now try to find an infection.
3640
				require_once($sourcedir . '/Subs-Graphics.php');
3641
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3642
				{
3643
					// It's bad. Try to re-encode the contents?
3644
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3645
					{
3646
						@unlink($_FILES['attachment']['tmp_name']);
3647
						return 'bad_avatar_fail_reencode';
3648
					}
3649
					// We were successful. However, at what price?
3650
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3651
					// Hard to believe this would happen, but can you bet?
3652
					if ($sizes === false)
3653
					{
3654
						@unlink($_FILES['attachment']['tmp_name']);
3655
						return 'bad_avatar';
3656
					}
3657
				}
3658
3659
				$extensions = array(
3660
					'1' => 'gif',
3661
					'2' => 'jpg',
3662
					'3' => 'png',
3663
					'6' => 'bmp'
3664
				);
3665
3666
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3667
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3668
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3669
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3670
				$file_hash = '';
3671
3672
				// Remove previous attachments this member might have had.
3673
				removeAttachments(array('id_member' => $memID));
3674
3675
				$cur_profile['id_attach'] = $smcFunc['db_insert']('',
3676
					'{db_prefix}attachments',
3677
					array(
3678
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3679
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3680
					),
3681
					array(
3682
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3683
						(int) $width, (int) $height, $mime_type, $id_folder,
3684
					),
3685
					array('id_attach'),
3686
					1
3687
				);
3688
3689
				$cur_profile['filename'] = $destName;
3690
				$cur_profile['attachment_type'] = 1;
3691
3692
				$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...
3693
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3694
				{
3695
					// I guess a man can try.
3696
					removeAttachments(array('id_member' => $memID));
3697
					fatal_lang_error('attach_timeout', 'critical');
3698
				}
3699
3700
				// Attempt to chmod it.
3701
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3702
			}
3703
			$profile_vars['avatar'] = '';
3704
3705
			// Delete any temporary file.
3706
			if (file_exists($_FILES['attachment']['tmp_name']))
3707
				@unlink($_FILES['attachment']['tmp_name']);
3708
		}
3709
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3710
		else
3711
			$profile_vars['avatar'] = '';
3712
	}
3713
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3714
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3715
3716
	else
3717
		$profile_vars['avatar'] = '';
3718
3719
	// Setup the profile variables so it shows things right on display!
3720
	$cur_profile['avatar'] = $profile_vars['avatar'];
3721
3722
	call_integration_hook('after_profile_save_avatar');
3723
3724
	return false;
3725
}
3726
3727
/**
3728
 * Validate the signature
3729
 *
3730
 * @param string &$value The new signature
3731
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3732
 */
3733
function profileValidateSignature(&$value)
3734
{
3735
	global $sourcedir, $modSettings, $smcFunc, $txt;
3736
3737
	require_once($sourcedir . '/Subs-Post.php');
3738
3739
	// Admins can do whatever they hell they want!
3740
	if (!allowedTo('admin_forum'))
3741
	{
3742
		// Load all the signature limits.
3743
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3744
		$sig_limits = explode(',', $sig_limits);
3745
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3746
3747
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3748
3749
		// Too many lines?
3750
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3751
		{
3752
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3753
			return 'signature_max_lines';
3754
		}
3755
3756
		// Too many images?!
3757
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
3758
		{
3759
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3760
			return 'signature_max_image_count';
3761
		}
3762
3763
		// What about too many smileys!
3764
		$smiley_parsed = $unparsed_signature;
3765
		parsesmileys($smiley_parsed);
3766
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3767
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3768
			return 'signature_allow_smileys';
3769
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3770
		{
3771
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3772
			return 'signature_max_smileys';
3773
		}
3774
3775
		// Maybe we are abusing font sizes?
3776
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3777
		{
3778
			foreach ($matches[1] as $ind => $size)
3779
			{
3780
				$limit_broke = 0;
3781
				// Attempt to allow all sizes of abuse, so to speak.
3782
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
3783
					$limit_broke = $sig_limits[7] . 'px';
3784
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3785
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3786
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3787
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3788
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3789
					$limit_broke = 'large';
3790
3791
				if ($limit_broke)
3792
				{
3793
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3794
					return 'signature_max_font_size';
3795
				}
3796
			}
3797
		}
3798
3799
		// The difficult one - image sizes! Don't error on this - just fix it.
3800
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3801
		{
3802
			// Get all BBC tags...
3803
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3804
			// ... and all HTML ones.
3805
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?' . '>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3806
			// And stick the HTML in the BBC.
3807
			if (!empty($matches2))
3808
			{
3809
				foreach ($matches2[0] as $ind => $dummy)
3810
				{
3811
					$matches[0][] = $matches2[0][$ind];
3812
					$matches[1][] = '';
3813
					$matches[2][] = '';
3814
					$matches[3][] = '';
3815
					$matches[4][] = '';
3816
					$matches[5][] = '';
3817
					$matches[6][] = '';
3818
					$matches[7][] = $matches2[1][$ind];
3819
				}
3820
			}
3821
3822
			$replaces = array();
3823
			// Try to find all the images!
3824
			if (!empty($matches))
3825
			{
3826
				foreach ($matches[0] as $key => $image)
3827
				{
3828
					$width = -1;
3829
					$height = -1;
3830
3831
					// Does it have predefined restraints? Width first.
3832
					if ($matches[6][$key])
3833
						$matches[2][$key] = $matches[6][$key];
3834
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
3835
					{
3836
						$width = $sig_limits[5];
3837
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3838
					}
3839
					elseif ($matches[2][$key])
3840
						$width = $matches[2][$key];
3841
					// ... and height.
3842
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
3843
					{
3844
						$height = $sig_limits[6];
3845
						if ($width != -1)
3846
							$width = $width * ($height / $matches[4][$key]);
3847
					}
3848
					elseif ($matches[4][$key])
3849
						$height = $matches[4][$key];
3850
3851
					// If the dimensions are still not fixed - we need to check the actual image.
3852
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
3853
					{
3854
						$sizes = url_image_size($matches[7][$key]);
3855
						if (is_array($sizes))
3856
						{
3857
							// Too wide?
3858
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3859
							{
3860
								$width = $sig_limits[5];
3861
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3862
							}
3863
							// Too high?
3864
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3865
							{
3866
								$height = $sig_limits[6];
3867
								if ($width == -1)
3868
									$width = $sizes[0];
3869
								$width = $width * ($height / $sizes[1]);
3870
							}
3871
							elseif ($width != -1)
3872
								$height = $sizes[1];
3873
						}
3874
					}
3875
3876
					// Did we come up with some changes? If so remake the string.
3877
					if ($width != -1 || $height != -1)
3878
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3879
				}
3880
				if (!empty($replaces))
3881
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3882
			}
3883
		}
3884
3885
		// Any disabled BBC?
3886
		$disabledSigBBC = implode('|', $disabledTags);
3887
		if (!empty($disabledSigBBC))
3888
		{
3889
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3890
			{
3891
				$disabledTags = array_unique($disabledTags);
3892
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3893
				return 'signature_disabled_bbc';
3894
			}
3895
		}
3896
	}
3897
3898
	preparsecode($value);
3899
3900
	// Too long?
3901
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3902
	{
3903
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3904
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3905
		return 'signature_max_length';
3906
	}
3907
3908
	return true;
3909
}
3910
3911
/**
3912
 * Validate an email address.
3913
 *
3914
 * @param string $email The email address to validate
3915
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3916
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3917
 */
3918
function profileValidateEmail($email, $memID = 0)
3919
{
3920
	global $smcFunc;
3921
3922
	$email = strtr($email, array('&#039;' => '\''));
3923
3924
	// Check the name and email for validity.
3925
	if (trim($email) == '')
3926
		return 'no_email';
3927
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3928
		return 'bad_email';
3929
3930
	// Email addresses should be and stay unique.
3931
	$request = $smcFunc['db_query']('', '
3932
		SELECT id_member
3933
		FROM {db_prefix}members
3934
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3935
			email_address = {string:email_address}
3936
		LIMIT 1',
3937
		array(
3938
			'selected_member' => $memID,
3939
			'email_address' => $email,
3940
		)
3941
	);
3942
3943
	if ($smcFunc['db_num_rows']($request) > 0)
3944
		return 'email_taken';
3945
	$smcFunc['db_free_result']($request);
3946
3947
	return true;
3948
}
3949
3950
/**
3951
 * Reload a user's settings.
3952
 */
3953
function profileReloadUser()
3954
{
3955
	global $modSettings, $context, $cur_profile;
3956
3957
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3958
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3959
3960
	loadUserSettings();
3961
	writeLog();
3962
}
3963
3964
/**
3965
 * Send the user a new activation email if they need to reactivate!
3966
 */
3967
function profileSendActivation()
3968
{
3969
	global $sourcedir, $profile_vars, $context, $scripturl, $smcFunc, $cookiename, $cur_profile, $language, $modSettings;
3970
3971
	require_once($sourcedir . '/Subs-Post.php');
3972
3973
	// Shouldn't happen but just in case.
3974
	if (empty($profile_vars['email_address']))
3975
		return;
3976
3977
	$replacements = array(
3978
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3979
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3980
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3981
	);
3982
3983
	// Send off the email.
3984
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3985
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3986
3987
	// Log the user out.
3988
	$smcFunc['db_query']('', '
3989
		DELETE FROM {db_prefix}log_online
3990
		WHERE id_member = {int:selected_member}',
3991
		array(
3992
			'selected_member' => $context['id_member'],
3993
		)
3994
	);
3995
	$_SESSION['log_time'] = 0;
3996
	$_SESSION['login_' . $cookiename] = $smcFunc['json_encode'](array(0, '', 0));
3997
3998
	if (isset($_COOKIE[$cookiename]))
3999
		$_COOKIE[$cookiename] = '';
4000
4001
	loadUserSettings();
4002
4003
	$context['user']['is_logged'] = false;
4004
	$context['user']['is_guest'] = true;
4005
4006
	redirectexit('action=sendactivation');
4007
}
4008
4009
/**
4010
 * Function to allow the user to choose group membership etc...
4011
 *
4012
 * @param int $memID The ID of the member
4013
 */
4014
function groupMembership($memID)
4015
{
4016
	global $txt, $user_profile, $context, $smcFunc;
4017
4018
	$curMember = $user_profile[$memID];
4019
	$context['primary_group'] = $curMember['id_group'];
4020
4021
	// Can they manage groups?
4022
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
4023
	$context['can_manage_protected'] = allowedTo('admin_forum');
4024
	$context['can_edit_primary'] = $context['can_manage_protected'];
4025
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
4026
4027
	// Get all the groups this user is a member of.
4028
	$groups = explode(',', $curMember['additional_groups']);
4029
	$groups[] = $curMember['id_group'];
4030
4031
	// Ensure the query doesn't croak!
4032
	if (empty($groups))
4033
		$groups = array(0);
4034
	// Just to be sure...
4035
	foreach ($groups as $k => $v)
4036
		$groups[$k] = (int) $v;
4037
4038
	// Get all the membergroups they can join.
4039
	$request = $smcFunc['db_query']('', '
4040
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
4041
			COALESCE(lgr.id_member, 0) AS pending
4042
		FROM {db_prefix}membergroups AS mg
4043
			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})
4044
		WHERE (mg.id_group IN ({array_int:group_list})
4045
			OR mg.group_type > {int:nonjoin_group_id})
4046
			AND mg.min_posts = {int:min_posts}
4047
			AND mg.id_group != {int:moderator_group}
4048
		ORDER BY group_name',
4049
		array(
4050
			'group_list' => $groups,
4051
			'selected_member' => $memID,
4052
			'status_open' => 0,
4053
			'nonjoin_group_id' => 1,
4054
			'min_posts' => -1,
4055
			'moderator_group' => 3,
4056
		)
4057
	);
4058
	// This beast will be our group holder.
4059
	$context['groups'] = array(
4060
		'member' => array(),
4061
		'available' => array()
4062
	);
4063
	while ($row = $smcFunc['db_fetch_assoc']($request))
4064
	{
4065
		// Can they edit their primary group?
4066
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
4067
			$context['can_edit_primary'] = true;
4068
4069
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
4070
		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...
4071
			continue;
4072
4073
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
4074
			'id' => $row['id_group'],
4075
			'name' => $row['group_name'],
4076
			'desc' => $row['description'],
4077
			'color' => $row['online_color'],
4078
			'type' => $row['group_type'],
4079
			'pending' => $row['pending'],
4080
			'is_primary' => $row['id_group'] == $context['primary_group'],
4081
			'can_be_primary' => $row['hidden'] != 2,
4082
			// Anything more than this needs to be done through account settings for security.
4083
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
4084
		);
4085
	}
4086
	$smcFunc['db_free_result']($request);
4087
4088
	// Add registered members on the end.
4089
	$context['groups']['member'][0] = array(
4090
		'id' => 0,
4091
		'name' => $txt['regular_members'],
4092
		'desc' => $txt['regular_members_desc'],
4093
		'type' => 0,
4094
		'is_primary' => $context['primary_group'] == 0 ? true : false,
4095
		'can_be_primary' => true,
4096
		'can_leave' => 0,
4097
	);
4098
4099
	// No changing primary one unless you have enough groups!
4100
	if (count($context['groups']['member']) < 2)
4101
		$context['can_edit_primary'] = false;
4102
4103
	// In the special case that someone is requesting membership of a group, setup some special context vars.
4104
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
4105
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
4106
}
4107
4108
/**
4109
 * This function actually makes all the group changes
4110
 *
4111
 * @param array $profile_vars The profile variables
4112
 * @param array $post_errors Any errors that have occurred
4113
 * @param int $memID The ID of the member
4114
 * @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
4115
 */
4116
function groupMembership2($profile_vars, $post_errors, $memID)
4117
{
4118
	global $user_info, $context, $user_profile, $modSettings, $smcFunc;
4119
4120
	// Let's be extra cautious...
4121
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
4122
		isAllowedTo('manage_membergroups');
4123
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
4124
		fatal_lang_error('no_access', false);
4125
4126
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
4127
4128
	$old_profile = &$user_profile[$memID];
4129
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
4130
	$context['can_manage_protected'] = allowedTo('admin_forum');
4131
4132
	// By default the new primary is the old one.
4133
	$newPrimary = $old_profile['id_group'];
4134
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
4135
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
4136
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
4137
4138
	// One way or another, we have a target group in mind...
4139
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
4140
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
4141
4142
	// Sanity check!!
4143
	if ($group_id == 1)
4144
		isAllowedTo('admin_forum');
4145
	// Protected groups too!
4146
	else
4147
	{
4148
		$request = $smcFunc['db_query']('', '
4149
			SELECT group_type
4150
			FROM {db_prefix}membergroups
4151
			WHERE id_group = {int:current_group}
4152
			LIMIT {int:limit}',
4153
			array(
4154
				'current_group' => $group_id,
4155
				'limit' => 1,
4156
			)
4157
		);
4158
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
4159
		$smcFunc['db_free_result']($request);
4160
4161
		if ($is_protected == 1)
4162
			isAllowedTo('admin_forum');
4163
	}
4164
4165
	// What ever we are doing, we need to determine if changing primary is possible!
4166
	$request = $smcFunc['db_query']('', '
4167
		SELECT id_group, group_type, hidden, group_name
4168
		FROM {db_prefix}membergroups
4169
		WHERE id_group IN ({int:group_list}, {int:current_group})',
4170
		array(
4171
			'group_list' => $group_id,
4172
			'current_group' => $old_profile['id_group'],
4173
		)
4174
	);
4175
	while ($row = $smcFunc['db_fetch_assoc']($request))
4176
	{
4177
		// Is this the new group?
4178
		if ($row['id_group'] == $group_id)
4179
		{
4180
			$foundTarget = true;
4181
			$group_name = $row['group_name'];
4182
4183
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
4184
			if ($changeType == 'request' && $row['group_type'] != 2)
4185
				fatal_lang_error('no_access', false);
4186
			// What about leaving a requestable group we are not a member of?
4187
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
4188
				fatal_lang_error('no_access', false);
4189
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
4190
				fatal_lang_error('no_access', false);
4191
4192
			// We can't change the primary group if this is hidden!
4193
			if ($row['hidden'] == 2)
4194
				$canChangePrimary = false;
4195
		}
4196
4197
		// If this is their old primary, can we change it?
4198
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
4199
			$canChangePrimary = 1;
4200
4201
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
4202
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
4203
			$canChangePrimary = false;
4204
4205
		// If this is the one we are acting on, can we even act?
4206
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
4207
			$canChangePrimary = false;
4208
	}
4209
	$smcFunc['db_free_result']($request);
4210
4211
	// Didn't find the target?
4212
	if (!$foundTarget)
4213
		fatal_lang_error('no_access', false);
4214
4215
	// Final security check, don't allow users to promote themselves to admin.
4216
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
4217
	{
4218
		$request = $smcFunc['db_query']('', '
4219
			SELECT COUNT(*)
4220
			FROM {db_prefix}permissions
4221
			WHERE id_group = {int:selected_group}
4222
				AND permission = {string:admin_forum}
4223
				AND add_deny = {int:not_denied}',
4224
			array(
4225
				'selected_group' => $group_id,
4226
				'not_denied' => 1,
4227
				'admin_forum' => 'admin_forum',
4228
			)
4229
		);
4230
		list ($disallow) = $smcFunc['db_fetch_row']($request);
4231
		$smcFunc['db_free_result']($request);
4232
4233
		if ($disallow)
4234
			isAllowedTo('admin_forum');
4235
	}
4236
4237
	// If we're requesting, add the note then return.
4238
	if ($changeType == 'request')
4239
	{
4240
		$request = $smcFunc['db_query']('', '
4241
			SELECT id_member
4242
			FROM {db_prefix}log_group_requests
4243
			WHERE id_member = {int:selected_member}
4244
				AND id_group = {int:selected_group}
4245
				AND status = {int:status_open}',
4246
			array(
4247
				'selected_member' => $memID,
4248
				'selected_group' => $group_id,
4249
				'status_open' => 0,
4250
			)
4251
		);
4252
		if ($smcFunc['db_num_rows']($request) != 0)
4253
			fatal_lang_error('profile_error_already_requested_group');
4254
		$smcFunc['db_free_result']($request);
4255
4256
		// Log the request.
4257
		$smcFunc['db_insert']('',
4258
			'{db_prefix}log_group_requests',
4259
			array(
4260
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
4261
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
4262
			),
4263
			array(
4264
				$memID, $group_id, time(), $_POST['reason'],
4265
				0, 0, '', 0, '',
4266
			),
4267
			array('id_request')
4268
		);
4269
4270
		// Set up some data for our background task...
4271
		$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...
4272
4273
		// Add a background task to handle notifying people of this request
4274
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
4275
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
4276
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
4277
		);
4278
4279
		return $changeType;
4280
	}
4281
	// Otherwise we are leaving/joining a group.
4282
	elseif ($changeType == 'free')
4283
	{
4284
		// Are we leaving?
4285
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
4286
		{
4287
			if ($old_profile['id_group'] == $group_id)
4288
				$newPrimary = 0;
4289
			else
4290
				unset($addGroups[$group_id]);
4291
		}
4292
		// ... if not, must be joining.
4293
		else
4294
		{
4295
			// Can we change the primary, and do we want to?
4296
			if ($canChangePrimary)
4297
			{
4298
				if ($old_profile['id_group'] != 0)
4299
					$addGroups[$old_profile['id_group']] = -1;
4300
				$newPrimary = $group_id;
4301
			}
4302
			// Otherwise it's an additional group...
4303
			else
4304
				$addGroups[$group_id] = -1;
4305
		}
4306
	}
4307
	// Finally, we must be setting the primary.
4308
	elseif ($canChangePrimary)
4309
	{
4310
		if ($old_profile['id_group'] != 0)
4311
			$addGroups[$old_profile['id_group']] = -1;
4312
		if (isset($addGroups[$group_id]))
4313
			unset($addGroups[$group_id]);
4314
		$newPrimary = $group_id;
4315
	}
4316
4317
	// Finally, we can make the changes!
4318
	foreach ($addGroups as $id => $dummy)
4319
		if (empty($id))
4320
			unset($addGroups[$id]);
4321
	$addGroups = implode(',', array_flip($addGroups));
4322
4323
	// Ensure that we don't cache permissions if the group is changing.
4324
	if ($context['user']['is_owner'])
4325
		$_SESSION['mc']['time'] = 0;
4326
	else
4327
		updateSettings(array('settings_updated' => time()));
4328
4329
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
4330
4331
	return $changeType;
4332
}
4333
4334
/**
4335
 * Provides interface to setup Two Factor Auth in SMF
4336
 *
4337
 * @param int $memID The ID of the member
4338
 */
4339
function tfasetup($memID)
4340
{
4341
	global $user_info, $context, $user_settings, $sourcedir, $modSettings, $smcFunc;
4342
4343
	require_once($sourcedir . '/Class-TOTP.php');
4344
	require_once($sourcedir . '/Subs-Auth.php');
4345
4346
	// load JS lib for QR
4347
	loadJavaScriptFile('qrcode.js', array('force_current' => false, 'validate' => true));
4348
4349
	// If TFA has not been setup, allow them to set it up
4350
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4351
	{
4352
		// Check to ensure we're forcing SSL for authentication
4353
		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...
4354
			fatal_lang_error('login_ssl_required', false);
4355
4356
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4357
		// we do not want too much AJAX to confuse them.
4358
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4359
		{
4360
			$context['from_ajax'] = true;
4361
			$context['template_layers'] = array();
4362
		}
4363
4364
		// When the code is being sent, verify to make sure the user got it right
4365
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4366
		{
4367
			$code = $_POST['tfa_code'];
4368
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4369
			$totp->setRange(1);
4370
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4371
4372
			if (empty($context['password_auth_failed']) && $valid_code)
4373
			{
4374
				$backup = substr(sha1($smcFunc['random_int']()), 0, 16);
4375
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4376
4377
				updateMemberData($memID, array(
4378
					'tfa_secret' => $_SESSION['tfa_secret'],
4379
					'tfa_backup' => $backup_encrypted,
4380
				));
4381
4382
				setTFACookie(3153600, $memID, hash_salt($backup_encrypted, $user_settings['password_salt']));
4383
4384
				unset($_SESSION['tfa_secret']);
4385
4386
				$context['tfa_backup'] = $backup;
4387
				$context['sub_template'] = 'tfasetup_backup';
4388
4389
				return;
4390
			}
4391
			else
4392
			{
4393
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4394
				$context['tfa_error'] = !$valid_code;
4395
				$context['tfa_pass_value'] = $_POST['oldpasswrd'];
4396
				$context['tfa_value'] = $_POST['tfa_code'];
4397
			}
4398
		}
4399
		else
4400
		{
4401
			$totp = new \TOTP\Auth();
4402
			$secret = $totp->generateCode();
4403
			$_SESSION['tfa_secret'] = $secret;
4404
			$context['tfa_secret'] = $secret;
4405
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4406
		}
4407
4408
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4409
	}
4410
	else
4411
		redirectexit('action=profile;area=account;u=' . $memID);
4412
}
4413
4414
/**
4415
 * Provides interface to disable two-factor authentication in SMF
4416
 *
4417
 * @param int $memID The ID of the member
4418
 */
4419
function tfadisable($memID)
4420
{
4421
	global $context, $modSettings, $smcFunc, $user_settings;
4422
4423
	if (!empty($user_settings['tfa_secret']))
4424
	{
4425
		// Bail if we're forcing SSL for authentication and the network connection isn't secure.
4426
		if (!empty($modSettings['force_ssl']) && !httpsOn())
4427
			fatal_lang_error('login_ssl_required', false);
4428
4429
		// The admin giveth...
4430
		elseif ($modSettings['tfa_mode'] == 3 && $context['user']['is_owner'])
4431
			fatal_lang_error('cannot_disable_tfa', false);
4432
		elseif ($modSettings['tfa_mode'] == 2 && $context['user']['is_owner'])
4433
		{
4434
			$groups = array($user_settings['id_group']);
4435
			if (!empty($user_settings['additional_groups']))
4436
				$groups = array_unique(array_merge($groups, explode(',', $user_settings['additional_groups'])));
4437
4438
			$request = $smcFunc['db_query']('', '
4439
				SELECT id_group
4440
				FROM {db_prefix}membergroups
4441
				WHERE tfa_required = {int:tfa_required}
4442
					AND id_group IN ({array_int:groups})',
4443
				array(
4444
					'tfa_required' => 1,
4445
					'groups' => $groups,
4446
				)
4447
			);
4448
			$tfa_required_groups = $smcFunc['db_num_rows']($request);
4449
			$smcFunc['db_free_result']($request);
4450
4451
			// They belong to a membergroup that requires tfa.
4452
			if (!empty($tfa_required_groups))
4453
				fatal_lang_error('cannot_disable_tfa2', false);
4454
		}
4455
	}
4456
	else
4457
		redirectexit('action=profile;area=account;u=' . $memID);
4458
}
4459
4460
?>