profileSendActivation()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Loading history...
Unused Code introduced by
The import $context is not used and could be removed.

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

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

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

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

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

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

3430
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, /** @scrutinizer ignore-type */ false, null, true);
Loading history...
3431
		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...
3432
		{
3433
			fwrite($tmpAvatar, $contents);
3434
			fclose($tmpAvatar);
3435
3436
			$downloadedExternalAvatar = true;
3437
			$_FILES['attachment']['tmp_name'] = $new_filename;
3438
		}
3439
	}
3440
3441
	// Removes whatever attachment there was before updating
3442
	if ($value == 'none')
3443
	{
3444
		$profile_vars['avatar'] = '';
3445
3446
		// Reset the attach ID.
3447
		$cur_profile['id_attach'] = 0;
3448
		$cur_profile['attachment_type'] = 0;
3449
		$cur_profile['filename'] = '';
3450
3451
		removeAttachments(array('id_member' => $memID));
3452
	}
3453
3454
	// An avatar from the server-stored galleries.
3455
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3456
	{
3457
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3458
		$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']) : '';
3459
3460
		// Clear current profile...
3461
		$cur_profile['id_attach'] = 0;
3462
		$cur_profile['attachment_type'] = 0;
3463
		$cur_profile['filename'] = '';
3464
3465
		// Get rid of their old avatar. (if uploaded.)
3466
		removeAttachments(array('id_member' => $memID));
3467
	}
3468
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3469
	{
3470
		// 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.
3471
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3472
			$profile_vars['avatar'] = 'gravatar://';
3473
		else
3474
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3475
3476
		// Get rid of their old avatar. (if uploaded.)
3477
		removeAttachments(array('id_member' => $memID));
3478
	}
3479
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3480
	{
3481
		// We need these clean...
3482
		$cur_profile['id_attach'] = 0;
3483
		$cur_profile['attachment_type'] = 0;
3484
		$cur_profile['filename'] = '';
3485
3486
		// Remove any attached avatar...
3487
		removeAttachments(array('id_member' => $memID));
3488
3489
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3490
		$mime_valid = check_mime_type($profile_vars['avatar'], 'image/', true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $is_path of check_mime_type(). ( Ignorable by Annotation )

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

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