alert_notifications_topics()   B
last analyzed

Complexity

Conditions 7
Paths 2

Size

Total Lines 152
Code Lines 98

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 98
c 0
b 0
f 0
nop 1
dl 0
loc 152
rs 7.1103
nc 2

How to fix   Long Method   

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 2025 Simple Machines and individual contributors
13
 * @license https://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1.5
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', 'webp');
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_boards']) && !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 = html_entity_decode($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) || $valueReference != htmlspecialchars($valueReference, 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 && strcasecmp($extension, 'webp') != 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
			AND id_member = {int:memID}',
2314
		array(
2315
			'memID' => $memID,
2316
			'read' => $read == 1 ? time() : 0,
2317
			'toMark' => $toMark,
2318
		)
2319
	);
2320
2321
	// Gotta know how many unread alerts are left.
2322
	$count = alert_count($memID, true);
2323
2324
	updateMemberData($memID, array('alerts' => $count));
2325
2326
	// Might want to know this.
2327
	return $count;
2328
}
2329
2330
/**
2331
 * Deletes a single or a group of alerts by ID
2332
 *
2333
 * @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...
2334
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2335
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
2336
 */
2337
function alert_delete($toDelete, $memID = false)
2338
{
2339
	global $smcFunc;
2340
2341
	if (empty($toDelete))
2342
		return false;
2343
2344
	$toDelete = (array) $toDelete;
2345
2346
	$smcFunc['db_query']('', '
2347
		DELETE FROM {db_prefix}user_alerts
2348
		WHERE id_alert IN({array_int:toDelete})
2349
			AND id_member = {int:memID}',
2350
		array(
2351
			'memID' => $memID,
2352
			'toDelete' => $toDelete,
2353
		)
2354
	);
2355
2356
	// Gotta know how many unread alerts are left.
2357
	if ($memID)
2358
	{
2359
		$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

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

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

3545
		$mime_type = get_mime_type($profile_vars['avatar'], /** @scrutinizer ignore-type */ true);
Loading history...
3546
		$mime_valid = strpos($mime_type, 'image/') === 0;
0 ignored issues
show
Bug introduced by
It seems like $mime_type can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, 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

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