Completed
Pull Request — release-2.1 (#4834)
by Jeremy
16:45 queued 08:07
created

Profile-Modify.php ➔ alert_count()   D

Complexity

Conditions 16
Paths 62

Size

Total Lines 87

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 62
nop 2
dl 0
loc 87
rs 4.7369
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has the primary job of showing and editing people's profiles.
5
 * 	It also allows the user to change some of their or another's preferences,
6
 * 	and such things
7
 *
8
 * Simple Machines Forum (SMF)
9
 *
10
 * @package SMF
11
 * @author Simple Machines http://www.simplemachines.org
12
 * @copyright 2018 Simple Machines and individual contributors
13
 * @license http://www.simplemachines.org/about/smf/license.php BSD
14
 *
15
 * @version 2.1 Beta 4
16
 */
17
18
if (!defined('SMF'))
19
	die('No direct access...');
20
21
/**
22
 * This defines every profile field known to man.
23
 *
24
 * @param bool $force_reload Whether to reload the data
25
 */
26
function loadProfileFields($force_reload = false)
27
{
28
	global $context, $profile_fields, $txt, $scripturl, $modSettings, $user_info, $smcFunc, $cur_profile, $language;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
29
	global $sourcedir, $profile_vars;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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?)
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
126
				if (preg_match('/(\d{4})[\-\., ](\d{2})[\-\., ](\d{2})/', $value, $dates) === 1)
127
				{
128
					$value = checkdate($dates[2], $dates[3], $dates[1] < 4 ? 4 : $dates[1]) ? sprintf('%04d-%02d-%02d', $dates[1] < 4 ? 4 : $dates[1], $dates[2], $dates[3]) : '1004-01-01';
129
					return true;
130
				}
131
				else
132
				{
133
					$value = empty($cur_profile['birthdate']) ? '1004-01-01' : $cur_profile['birthdate'];
134
					return false;
135
				}
136
			},
137
		),
138
		'date_registered' => array(
139
			'type' => 'date',
140
			'value' => empty($cur_profile['date_registered']) ? $txt['not_applicable'] : strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600),
141
			'label' => $txt['date_registered'],
142
			'log_change' => true,
143
			'permission' => 'moderate_forum',
144
			'input_validate' => function(&$value) use ($txt, $user_info, $modSettings, $cur_profile, $context)
145
			{
146
				// Bad date!  Go try again - please?
147
				if (($value = strtotime($value)) === -1)
148
				{
149
					$value = $cur_profile['date_registered'];
150
					return $txt['invalid_registration'] . ' ' . strftime('%d %b %Y ' . (strpos($user_info['time_format'], '%H') !== false ? '%I:%M:%S %p' : '%H:%M:%S'), forum_time(false));
151
				}
152
				// As long as it doesn't equal "N/A"...
153
				elseif ($value != $txt['not_applicable'] && $value != strtotime(strftime('%Y-%m-%d', $cur_profile['date_registered'] + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600)))
154
					$value = $value - ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
155
				else
156
					$value = $cur_profile['date_registered'];
157
158
				return true;
159
			},
160
		),
161
		'email_address' => array(
162
			'type' => 'email',
163
			'label' => $txt['user_email_address'],
164
			'subtext' => $txt['valid_email'],
165
			'log_change' => true,
166
			'permission' => 'profile_password',
167
			'js_submit' => !empty($modSettings['send_validation_onChange']) ? '
168
	form_handle.addEventListener(\'submit\', function(event)
169
	{
170
		if (this.email_address.value != "'. (!empty($cur_profile['email_address']) ? $cur_profile['email_address'] : '') . '")
171
		{
172
			alert('. JavaScriptEscape($txt['email_change_logout']) . ');
173
			return true;
174
		}
175
	}, false);' : '',
176
			'input_validate' => function(&$value)
177
			{
178
				global $context, $old_profile, $profile_vars, $sourcedir, $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
179
180
				if (strtolower($value) == strtolower($old_profile['email_address']))
181
					return false;
182
183
				$isValid = profileValidateEmail($value, $context['id_member']);
184
185
				// Do they need to revalidate? If so schedule the function!
186
				if ($isValid === true && !empty($modSettings['send_validation_onChange']) && !allowedTo('moderate_forum'))
187
				{
188
					require_once($sourcedir . '/Subs-Members.php');
189
					$profile_vars['validation_code'] = generateValidationCode();
190
					$profile_vars['is_activated'] = 2;
191
					$context['profile_execute_on_save'][] = 'profileSendActivation';
192
					unset($context['profile_execute_on_save']['reload_user']);
193
				}
194
195
				return $isValid;
196
			},
197
		),
198
		// Selecting group membership is a complicated one so we treat it separate!
199
		'id_group' => array(
200
			'type' => 'callback',
201
			'callback_func' => 'group_manage',
202
			'permission' => 'manage_membergroups',
203
			'preload' => 'profileLoadGroups',
204
			'log_change' => true,
205
			'input_validate' => 'profileSaveGroups',
206
		),
207
		'id_theme' => array(
208
			'type' => 'callback',
209
			'callback_func' => 'theme_pick',
210
			'permission' => 'profile_extra',
211
			'enabled' => $modSettings['theme_allow'] || allowedTo('admin_forum'),
212
			'preload' => function() use ($smcFunc, &$context, $cur_profile, $txt)
213
			{
214
				$request = $smcFunc['db_query']('', '
215
					SELECT value
216
					FROM {db_prefix}themes
217
					WHERE id_theme = {int:id_theme}
218
						AND variable = {string:variable}
219
					LIMIT 1', array(
220
						'id_theme' => $cur_profile['id_theme'],
221
						'variable' => 'name',
222
					)
223
				);
224
				list ($name) = $smcFunc['db_fetch_row']($request);
225
				$smcFunc['db_free_result']($request);
226
227
				$context['member']['theme'] = array(
228
					'id' => $cur_profile['id_theme'],
229
					'name' => empty($cur_profile['id_theme']) ? $txt['theme_forum_default'] : $name
230
				);
231
				return true;
232
			},
233
			'input_validate' => function(&$value)
234
			{
235
				$value = (int) $value;
236
				return true;
237
			},
238
		),
239
		'lngfile' => array(
240
			'type' => 'select',
241
			'options' => function() use (&$context)
242
			{
243
				return $context['profile_languages'];
244
			},
245
			'label' => $txt['preferred_language'],
246
			'permission' => 'profile_identity',
247
			'preload' => 'profileLoadLanguages',
248
			'enabled' => !empty($modSettings['userLanguage']),
249
			'value' => empty($cur_profile['lngfile']) ? $language : $cur_profile['lngfile'],
250
			'input_validate' => function(&$value) use (&$context, $cur_profile)
251
			{
252
				// Load the languages.
253
				profileLoadLanguages();
254
255
				if (isset($context['profile_languages'][$value]))
256
				{
257
					if ($context['user']['is_owner'] && empty($context['password_auth_failed']))
258
						$_SESSION['language'] = $value;
259
					return true;
260
				}
261
				else
262
				{
263
					$value = $cur_profile['lngfile'];
264
					return false;
265
				}
266
			},
267
		),
268
		// The username is not always editable - so adjust it as such.
269
		'member_name' => array(
270
			'type' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? 'text' : 'label',
271
			'label' => $txt['username'],
272
			'subtext' => allowedTo('admin_forum') && !isset($_GET['changeusername']) ? '[<a href="' . $scripturl . '?action=profile;u=' . $context['id_member'] . ';area=account;changeusername" style="font-style: italic;">' . $txt['username_change'] . '</a>]' : '',
273
			'log_change' => true,
274
			'permission' => 'profile_identity',
275
			'prehtml' => allowedTo('admin_forum') && isset($_GET['changeusername']) ? '<div class="alert">' . $txt['username_warning'] . '</div>' : '',
276
			'input_validate' => function(&$value) use ($sourcedir, $context, $user_info, $cur_profile)
277
			{
278
				if (allowedTo('admin_forum'))
279
				{
280
					// We'll need this...
281
					require_once($sourcedir . '/Subs-Auth.php');
282
283
					// Maybe they are trying to change their password as well?
284
					$resetPassword = true;
285
					if (isset($_POST['passwrd1']) && $_POST['passwrd1'] != '' && isset($_POST['passwrd2']) && $_POST['passwrd1'] == $_POST['passwrd2'] && validatePassword($_POST['passwrd1'], $value, array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email'])) == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing validatePassword($_POST[..., $user_info['email'])) of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
286
						$resetPassword = false;
287
288
					// Do the reset... this will send them an email too.
289
					if ($resetPassword)
290
						resetPassword($context['id_member'], $value);
291
					elseif ($value !== null)
292
					{
293
						validateUsername($context['id_member'], trim(preg_replace('~[\t\n\r \x0B\0' . ($context['utf8'] ? '\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}' : '\x00-\x08\x0B\x0C\x0E-\x19\xA0') . ']+~' . ($context['utf8'] ? 'u' : ''), ' ', $value)));
294
						updateMemberData($context['id_member'], array('member_name' => $value));
295
296
						// Call this here so any integrated systems will know about the name change (resetPassword() takes care of this if we're letting SMF generate the password)
297
						call_integration_hook('integrate_reset_pass', array($cur_profile['member_name'], $value, $_POST['passwrd1']));
298
					}
299
				}
300
				return false;
301
			},
302
		),
303
		'passwrd1' => array(
304
			'type' => 'password',
305
			'label' => ucwords($txt['choose_pass']),
306
			'subtext' => $txt['password_strength'],
307
			'size' => 20,
308
			'value' => '',
309
			'permission' => 'profile_password',
310
			'save_key' => 'passwd',
311
			// Note this will only work if passwrd2 also exists!
312
			'input_validate' => function(&$value) use ($sourcedir, $user_info, $smcFunc, $cur_profile)
313
			{
314
				// If we didn't try it then ignore it!
315
				if ($value == '')
316
					return false;
317
318
				// Do the two entries for the password even match?
319
				if (!isset($_POST['passwrd2']) || $value != $_POST['passwrd2'])
320
					return 'bad_new_password';
321
322
				// Let's get the validation function into play...
323
				require_once($sourcedir . '/Subs-Auth.php');
324
				$passwordErrors = validatePassword($value, $cur_profile['member_name'], array($cur_profile['real_name'], $user_info['username'], $user_info['name'], $user_info['email']));
325
326
				// Were there errors?
327
				if ($passwordErrors != null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $passwordErrors of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
328
					return 'password_' . $passwordErrors;
329
330
				// Set up the new password variable... ready for storage.
331
				$value = hash_password($cur_profile['member_name'], un_htmlspecialchars($value));
332
333
				return true;
334
			},
335
		),
336
		'passwrd2' => array(
337
			'type' => 'password',
338
			'label' => ucwords($txt['verify_pass']),
339
			'size' => 20,
340
			'value' => '',
341
			'permission' => 'profile_password',
342
			'is_dummy' => true,
343
		),
344
		'personal_text' => array(
345
			'type' => 'text',
346
			'label' => $txt['personal_text'],
347
			'log_change' => true,
348
			'input_attr' => array('maxlength="50"'),
349
			'size' => 50,
350
			'permission' => 'profile_blurb',
351
			'input_validate' => function(&$value) use ($smcFunc)
352
			{
353
				if ($smcFunc['strlen']($value) > 50)
354
					return 'personal_text_too_long';
355
356
				return true;
357
			},
358
		),
359
		// This does ALL the pm settings
360
		'pm_prefs' => array(
361
			'type' => 'callback',
362
			'callback_func' => 'pm_settings',
363
			'permission' => 'pm_read',
364
			'preload' => function() use (&$context, $cur_profile)
365
			{
366
				$context['display_mode'] = $cur_profile['pm_prefs'] & 3;
367
				$context['receive_from'] = !empty($cur_profile['pm_receive_from']) ? $cur_profile['pm_receive_from'] : 0;
368
369
				return true;
370
			},
371
			'input_validate' => function(&$value) use (&$cur_profile, &$profile_vars)
372
			{
373
				// Simple validate and apply the two "sub settings"
374
				$value = max(min($value, 2), 0);
375
376
				$cur_profile['pm_receive_from'] = $profile_vars['pm_receive_from'] = max(min((int) $_POST['pm_receive_from'], 4), 0);
377
378
				return true;
379
			},
380
		),
381
		'posts' => array(
382
			'type' => 'int',
383
			'label' => $txt['profile_posts'],
384
			'log_change' => true,
385
			'size' => 7,
386
			'permission' => 'moderate_forum',
387
			'input_validate' => function(&$value)
388
			{
389
				if (!is_numeric($value))
390
					return 'digits_only';
391
				else
392
					$value = $value != '' ? strtr($value, array(',' => '', '.' => '', ' ' => '')) : 0;
393
				return true;
394
			},
395
		),
396
		'real_name' => array(
397
			'type' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum') ? 'text' : 'label',
398
			'label' => $txt['name'],
399
			'subtext' => $txt['display_name_desc'],
400
			'log_change' => true,
401
			'input_attr' => array('maxlength="60"'),
402
			'permission' => 'profile_displayed_name',
403
			'enabled' => allowedTo('profile_displayed_name_own') || allowedTo('profile_displayed_name_any') || allowedTo('moderate_forum'),
404
			'input_validate' => function(&$value) use ($context, $smcFunc, $sourcedir, $cur_profile)
405
			{
406
				$value = trim(preg_replace('~[\t\n\r \x0B\0' . ($context['utf8'] ? '\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}' : '\x00-\x08\x0B\x0C\x0E-\x19\xA0') . ']+~' . ($context['utf8'] ? 'u' : ''), ' ', $value));
407
408
				if (trim($value) == '')
409
					return 'no_name';
410
				elseif ($smcFunc['strlen']($value) > 60)
411
					return 'name_too_long';
412
				elseif ($cur_profile['real_name'] != $value)
413
				{
414
					require_once($sourcedir . '/Subs-Members.php');
415
					if (isReservedName($value, $context['id_member']))
416
						return 'name_taken';
417
				}
418
				return true;
419
			},
420
		),
421
		'secret_question' => array(
422
			'type' => 'text',
423
			'label' => $txt['secret_question'],
424
			'subtext' => $txt['secret_desc'],
425
			'size' => 50,
426
			'permission' => 'profile_password',
427
		),
428
		'secret_answer' => array(
429
			'type' => 'text',
430
			'label' => $txt['secret_answer'],
431
			'subtext' => $txt['secret_desc2'],
432
			'size' => 20,
433
			'postinput' => '<span class="smalltext"><a href="' . $scripturl . '?action=helpadmin;help=secret_why_blank" onclick="return reqOverlayDiv(this.href);"><span class="generic_icons help"></span> ' . $txt['secret_why_blank'] . '</a></span>',
434
			'value' => '',
435
			'permission' => 'profile_password',
436
			'input_validate' => function(&$value)
437
			{
438
				$value = $value != '' ? md5($value) : '';
439
				return true;
440
			},
441
		),
442
		'signature' => array(
443
			'type' => 'callback',
444
			'callback_func' => 'signature_modify',
445
			'permission' => 'profile_signature',
446
			'enabled' => substr($modSettings['signature_settings'], 0, 1) == 1,
447
			'preload' => 'profileLoadSignatureData',
448
			'input_validate' => 'profileValidateSignature',
449
		),
450
		'show_online' => array(
451
			'type' => 'check',
452
			'label' => $txt['show_online'],
453
			'permission' => 'profile_identity',
454
			'enabled' => !empty($modSettings['allow_hideOnline']) || allowedTo('moderate_forum'),
455
		),
456
		'smiley_set' => array(
457
			'type' => 'callback',
458
			'callback_func' => 'smiley_pick',
459
			'enabled' => !empty($modSettings['smiley_sets_enable']),
460
			'permission' => 'profile_extra',
461
			'preload' => function() use ($modSettings, &$context, $txt, $cur_profile, $smcFunc)
462
			{
463
				$context['member']['smiley_set']['id'] = empty($cur_profile['smiley_set']) ? '' : $cur_profile['smiley_set'];
464
				$context['smiley_sets'] = explode(',', 'none,,' . $modSettings['smiley_sets_known']);
465
				$set_names = explode("\n", $txt['smileys_none'] . "\n" . $txt['smileys_forum_board_default'] . "\n" . $modSettings['smiley_sets_names']);
466
				foreach ($context['smiley_sets'] as $i => $set)
467
				{
468
					$context['smiley_sets'][$i] = array(
469
						'id' => $smcFunc['htmlspecialchars']($set),
470
						'name' => $smcFunc['htmlspecialchars']($set_names[$i]),
471
						'selected' => $set == $context['member']['smiley_set']['id']
472
					);
473
474
					if ($context['smiley_sets'][$i]['selected'])
475
						$context['member']['smiley_set']['name'] = $set_names[$i];
476
				}
477
				return true;
478
			},
479
			'input_validate' => function(&$value)
480
			{
481
				global $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
482
483
				$smiley_sets = explode(',', $modSettings['smiley_sets_known']);
484
				if (!in_array($value, $smiley_sets) && $value != 'none')
485
					$value = '';
486
				return true;
487
			},
488
		),
489
		// Pretty much a dummy entry - it populates all the theme settings.
490
		'theme_settings' => array(
491
			'type' => 'callback',
492
			'callback_func' => 'theme_settings',
493
			'permission' => 'profile_extra',
494
			'is_dummy' => true,
495
			'preload' => function() use (&$context, $user_info, $modSettings)
496
			{
497
				loadLanguage('Settings');
498
499
				$context['allow_no_censored'] = false;
500
				if ($user_info['is_admin'] || $context['user']['is_owner'])
501
					$context['allow_no_censored'] = !empty($modSettings['allow_no_censored']);
502
503
				return true;
504
			},
505
		),
506
		'tfa' => array(
507
			'type' => 'callback',
508
			'callback_func' => 'tfa',
509
			'permission' => 'profile_password',
510
			'enabled' => !empty($modSettings['tfa_mode']),
511
			'preload' => function() use (&$context, $cur_profile)
512
			{
513
				$context['tfa_enabled'] = !empty($cur_profile['tfa_secret']);
514
515
				return true;
516
			},
517
		),
518
		'time_format' => array(
519
			'type' => 'callback',
520
			'callback_func' => 'timeformat_modify',
521
			'permission' => 'profile_extra',
522
			'preload' => function() use (&$context, $user_info, $txt, $cur_profile, $modSettings)
523
			{
524
				$context['easy_timeformats'] = array(
525
					array('format' => '', 'title' => $txt['timeformat_default']),
526
					array('format' => '%B %d, %Y, %I:%M:%S %p', 'title' => $txt['timeformat_easy1']),
527
					array('format' => '%B %d, %Y, %H:%M:%S', 'title' => $txt['timeformat_easy2']),
528
					array('format' => '%Y-%m-%d, %H:%M:%S', 'title' => $txt['timeformat_easy3']),
529
					array('format' => '%d %B %Y, %H:%M:%S', 'title' => $txt['timeformat_easy4']),
530
					array('format' => '%d-%m-%Y, %H:%M:%S', 'title' => $txt['timeformat_easy5'])
531
				);
532
533
				$context['member']['time_format'] = $cur_profile['time_format'];
534
				$context['current_forum_time'] = timeformat(time() - $user_info['time_offset'] * 3600, false);
535
				$context['current_forum_time_js'] = strftime('%Y,' . ((int) strftime('%m', time() + $modSettings['time_offset'] * 3600) - 1) . ',%d,%H,%M,%S', time() + $modSettings['time_offset'] * 3600);
536
				$context['current_forum_time_hour'] = (int) strftime('%H', forum_time(false));
537
				return true;
538
			},
539
		),
540
		'timezone' => array(
541
			'type' => 'select',
542
			'options' => smf_list_timezones(),
543
			'permission' => 'profile_extra',
544
			'label' => $txt['timezone'],
545
			'input_validate' => function($value)
546
			{
547
				$tz = smf_list_timezones();
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $tz. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
548
				if (!isset($tz[$value]))
549
					return 'bad_timezone';
550
551
				return true;
552
			},
553
		),
554
		'usertitle' => array(
555
			'type' => 'text',
556
			'label' => $txt['custom_title'],
557
			'log_change' => true,
558
			'input_attr' => array('maxlength="50"'),
559
			'size' => 50,
560
			'permission' => 'profile_title',
561
			'enabled' => !empty($modSettings['titlesEnable']),
562
			'input_validate' => function(&$value) use ($smcFunc)
563
			{
564
				if ($smcFunc['strlen']($value) > 50)
565
					return 'user_title_too_long';
566
567
				return true;
568
			},
569
		),
570
		'website_title' => array(
571
			'type' => 'text',
572
			'label' => $txt['website_title'],
573
			'subtext' => $txt['include_website_url'],
574
			'size' => 50,
575
			'permission' => 'profile_website',
576
			'link_with' => 'website',
577
		),
578
		'website_url' => array(
579
			'type' => 'url',
580
			'label' => $txt['website_url'],
581
			'subtext' => $txt['complete_url'],
582
			'size' => 50,
583
			'permission' => 'profile_website',
584
			// Fix the URL...
585
			'input_validate' => function(&$value)
586
			{
587
				if (strlen(trim($value)) > 0 && strpos($value, '://') === false)
588
					$value = 'http://' . $value;
589 View Code Duplication
				if (strlen($value) < 8 || (substr($value, 0, 7) !== 'http://' && substr($value, 0, 8) !== 'https://'))
590
					$value = '';
591
				$value = (string) validate_iri(sanitize_iri($value));
0 ignored issues
show
Bug introduced by
It seems like sanitize_iri($value) targeting sanitize_iri() can also be of type boolean; however, validate_iri() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
592
				return true;
593
			},
594
			'link_with' => 'website',
595
		),
596
	);
597
598
	call_integration_hook('integrate_load_profile_fields', array(&$profile_fields));
599
600
	$disabled_fields = !empty($modSettings['disabled_profile_fields']) ? explode(',', $modSettings['disabled_profile_fields']) : array();
601
	// For each of the above let's take out the bits which don't apply - to save memory and security!
602
	foreach ($profile_fields as $key => $field)
603
	{
604
		// Do we have permission to do this?
605
		if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? array($field['permission'] . '_own', $field['permission'] . '_any') : $field['permission'] . '_any')) && !allowedTo($field['permission']))
606
			unset($profile_fields[$key]);
607
608
		// Is it enabled?
609
		if (isset($field['enabled']) && !$field['enabled'])
610
			unset($profile_fields[$key]);
611
612
		// Is it specifically disabled?
613
		if (in_array($key, $disabled_fields) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields)))
614
			unset($profile_fields[$key]);
615
	}
616
}
617
618
/**
619
 * Setup the context for a page load!
620
 *
621
 * @param array $fields The profile fields to display. Each item should correspond to an item in the $profile_fields array generated by loadProfileFields
622
 */
623
function setupProfileContext($fields)
624
{
625
	global $profile_fields, $context, $cur_profile, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
626
627
	// Some default bits.
628
	$context['profile_prehtml'] = '';
629
	$context['profile_posthtml'] = '';
630
	$context['profile_javascript'] = '';
631
	$context['profile_onsubmit_javascript'] = '';
632
633
	call_integration_hook('integrate_setup_profile_context', array(&$fields));
634
635
	// Make sure we have this!
636
	loadProfileFields(true);
637
638
	// First check for any linked sets.
639
	foreach ($profile_fields as $key => $field)
640
		if (isset($field['link_with']) && in_array($field['link_with'], $fields))
641
			$fields[] = $key;
642
643
	$i = 0;
644
	$last_type = '';
645
	foreach ($fields as $key => $field)
646
	{
647
		if (isset($profile_fields[$field]))
648
		{
649
			// Shortcut.
650
			$cur_field = &$profile_fields[$field];
651
652
			// Does it have a preload and does that preload succeed?
653
			if (isset($cur_field['preload']) && !$cur_field['preload']())
654
				continue;
655
656
			// If this is anything but complex we need to do more cleaning!
657
			if ($cur_field['type'] != 'callback' && $cur_field['type'] != 'hidden')
658
			{
659
				if (!isset($cur_field['label']))
660
					$cur_field['label'] = isset($txt[$field]) ? $txt[$field] : $field;
661
662
				// Everything has a value!
663
				if (!isset($cur_field['value']))
664
					$cur_field['value'] = isset($cur_profile[$field]) ? $cur_profile[$field] : '';
665
666
				// Any input attributes?
667
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
668
			}
669
670
			// Was there an error with this field on posting?
671
			if (isset($context['profile_errors'][$field]))
672
				$cur_field['is_error'] = true;
673
674
			// Any javascript stuff?
675
			if (!empty($cur_field['js_submit']))
676
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
677
			if (!empty($cur_field['js']))
678
				$context['profile_javascript'] .= $cur_field['js'];
679
680
			// Any template stuff?
681
			if (!empty($cur_field['prehtml']))
682
				$context['profile_prehtml'] .= $cur_field['prehtml'];
683
			if (!empty($cur_field['posthtml']))
684
				$context['profile_posthtml'] .= $cur_field['posthtml'];
685
686
			// Finally put it into context?
687
			if ($cur_field['type'] != 'hidden')
688
			{
689
				$last_type = $cur_field['type'];
690
				$context['profile_fields'][$field] = &$profile_fields[$field];
691
			}
692
		}
693
		// Bodge in a line break - without doing two in a row ;)
694
		elseif ($field == 'hr' && $last_type != 'hr' && $last_type != '')
695
		{
696
			$last_type = 'hr';
697
			$context['profile_fields'][$i++]['type'] = 'hr';
698
		}
699
	}
700
701
	// Some spicy JS.
702
	addInlineJavaScript('
703
	var form_handle = document.forms.creator;
704
	createEventListener(form_handle);
705
	'. (!empty($context['require_password']) ? '
706
	form_handle.addEventListener(\'submit\', function(event)
707
	{
708
		if (this.oldpasswrd.value == "")
709
		{
710
			event.preventDefault();
711
			alert('. (JavaScriptEscape($txt['required_security_reasons'])) . ');
712
			return false;
713
		}
714
	}, false);' : ''), true);
715
716
	// Any onsubmit javascript?
717
	if (!empty($context['profile_onsubmit_javascript']))
718
		addInlineJavaScript($context['profile_onsubmit_javascript'], true);
719
720
	// Any totally custom stuff?
721
	if (!empty($context['profile_javascript']))
722
		addInlineJavaScript($context['profile_javascript'], true);
723
724
	// Free up some memory.
725
	unset($profile_fields);
726
}
727
728
/**
729
 * Save the profile changes.
730
 */
731
function saveProfileFields()
732
{
733
	global $profile_fields, $profile_vars, $context, $old_profile, $post_errors, $cur_profile;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
734
735
	// Load them up.
736
	loadProfileFields();
737
738
	// This makes things easier...
739
	$old_profile = $cur_profile;
740
741
	// This allows variables to call activities when they save - by default just to reload their settings
742
	$context['profile_execute_on_save'] = array();
743
	if ($context['user']['is_owner'])
744
		$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
745
746
	// Assume we log nothing.
747
	$context['log_changes'] = array();
748
749
	// Cycle through the profile fields working out what to do!
750
	foreach ($profile_fields as $key => $field)
751
	{
752
		if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key == 'signature'))
753
			continue;
754
755
		// What gets updated?
756
		$db_key = isset($field['save_key']) ? $field['save_key'] : $key;
757
758
		// Right - we have something that is enabled, we can act upon and has a value posted to it. Does it have a validation function?
759
		if (isset($field['input_validate']))
760
		{
761
			$is_valid = $field['input_validate']($_POST[$key]);
762
			// An error occurred - set it as such!
763
			if ($is_valid !== true)
764
			{
765
				// Is this an actual error?
766
				if ($is_valid !== false)
767
				{
768
					$post_errors[$key] = $is_valid;
769
					$profile_fields[$key]['is_error'] = $is_valid;
770
				}
771
				// Retain the old value.
772
				$cur_profile[$key] = $_POST[$key];
773
				continue;
774
			}
775
		}
776
777
		// Are we doing a cast?
778
		$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
779
780
		// Finally, clean up certain types.
781
		if ($field['cast_type'] == 'int')
782
			$_POST[$key] = (int) $_POST[$key];
783
		elseif ($field['cast_type'] == 'float')
784
			$_POST[$key] = (float) $_POST[$key];
785
		elseif ($field['cast_type'] == 'check')
786
			$_POST[$key] = !empty($_POST[$key]) ? 1 : 0;
787
788
		// If we got here we're doing OK.
789
		if ($field['type'] != 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
790
		{
791
			// Set the save variable.
792
			$profile_vars[$db_key] = $_POST[$key];
793
			// And update the user profile.
794
			$cur_profile[$key] = $_POST[$key];
795
796
			// Are we logging it?
797
			if (!empty($field['log_change']) && isset($old_profile[$key]))
798
				$context['log_changes'][$key] = array(
799
					'previous' => $old_profile[$key],
800
					'new' => $_POST[$key],
801
				);
802
		}
803
804
		// Logging group changes are a bit different...
805
		if ($key == 'id_group' && $field['log_change'])
806
		{
807
			profileLoadGroups();
808
809
			// Any changes to primary group?
810
			if ($_POST['id_group'] != $old_profile['id_group'])
811
			{
812
				$context['log_changes']['id_group'] = array(
813
					'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
814
					'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
815
				);
816
			}
817
818
			// Prepare additional groups for comparison.
819
			$additional_groups = array(
820
				'previous' => !empty($old_profile['additional_groups']) ? explode(',', $old_profile['additional_groups']) : array(),
821
				'new' => !empty($_POST['additional_groups']) ? array_diff($_POST['additional_groups'], array(0)) : array(),
822
			);
823
824
			sort($additional_groups['previous']);
825
			sort($additional_groups['new']);
826
827
			// What about additional groups?
828
			if ($additional_groups['previous'] != $additional_groups['new'])
829
			{
830
				foreach ($additional_groups as $type => $groups)
831
				{
832
					foreach ($groups as $id => $group)
0 ignored issues
show
Bug introduced by
The expression $groups of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
833
					{
834
						if (isset($context['member_groups'][$group]))
835
							$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
836
						else
837
							unset($additional_groups[$type][$id]);
838
					}
839
					$additional_groups[$type] = implode(', ', $additional_groups[$type]);
840
				}
841
842
				$context['log_changes']['additional_groups'] = $additional_groups;
843
			}
844
		}
845
	}
846
847
	// @todo Temporary
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
848
	if ($context['user']['is_owner'])
849
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
850
	else
851
		$changeOther = allowedTo('profile_extra_any');
852
	if ($changeOther && empty($post_errors))
853
	{
854
		makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
855
		if (!empty($_REQUEST['sa']))
856
		{
857
			$custom_fields_errors = makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false, true);
858
859
			if (!empty($custom_fields_errors))
860
				$post_errors = array_merge($post_errors, $custom_fields_errors);
861
		}
862
	}
863
864
	// Free memory!
865
	unset($profile_fields);
866
}
867
868
/**
869
 * Save the profile changes
870
 *
871
 * @param array &$profile_vars The items to save
872
 * @param array &$post_errors An array of information about any errors that occurred
873
 * @param int $memID The ID of the member whose profile we're saving
874
 */
875
function saveProfileChanges(&$profile_vars, &$post_errors, $memID)
0 ignored issues
show
Unused Code introduced by
The parameter $post_errors is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
876
{
877
	global $user_profile, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
878
879
	// These make life easier....
880
	$old_profile = &$user_profile[$memID];
881
882
	// Permissions...
883
	if ($context['user']['is_owner'])
884
	{
885
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own', 'profile_website_any', 'profile_website_own', 'profile_signature_any', 'profile_signature_own'));
886
	}
887
	else
888
		$changeOther = allowedTo(array('profile_extra_any', 'profile_website_any', 'profile_signature_any'));
889
890
	// Arrays of all the changes - makes things easier.
891
	$profile_bools = array();
892
	$profile_ints = array();
893
	$profile_floats = array();
894
	$profile_strings = array(
895
		'buddy_list',
896
		'ignore_boards',
897
	);
898
899
	if (isset($_POST['sa']) && $_POST['sa'] == 'ignoreboards' && empty($_POST['ignore_brd']))
900
		$_POST['ignore_brd'] = array();
901
902
	unset($_POST['ignore_boards']); // Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
903
	if (isset($_POST['ignore_brd']))
904
	{
905
		if (!is_array($_POST['ignore_brd']))
906
			$_POST['ignore_brd'] = array($_POST['ignore_brd']);
907
908
		foreach ($_POST['ignore_brd'] as $k => $d)
909
		{
910
			$d = (int) $d;
911
			if ($d != 0)
912
				$_POST['ignore_brd'][$k] = $d;
913
			else
914
				unset($_POST['ignore_brd'][$k]);
915
		}
916
		$_POST['ignore_boards'] = implode(',', $_POST['ignore_brd']);
917
		unset($_POST['ignore_brd']);
918
	}
919
920
	// Here's where we sort out all the 'other' values...
921
	if ($changeOther)
922
	{
923
		makeThemeChanges($memID, isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
924
		//makeAvatarChanges($memID, $post_errors);
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
925
926
		if (!empty($_REQUEST['sa']))
927
			makeCustomFieldChanges($memID, $_REQUEST['sa'], false);
928
929
		foreach ($profile_bools as $var)
930
			if (isset($_POST[$var]))
931
				$profile_vars[$var] = empty($_POST[$var]) ? '0' : '1';
932
		foreach ($profile_ints as $var)
933
			if (isset($_POST[$var]))
934
				$profile_vars[$var] = $_POST[$var] != '' ? (int) $_POST[$var] : '';
935
		foreach ($profile_floats as $var)
936
			if (isset($_POST[$var]))
937
				$profile_vars[$var] = (float) $_POST[$var];
938
		foreach ($profile_strings as $var)
939
			if (isset($_POST[$var]))
940
				$profile_vars[$var] = $_POST[$var];
941
	}
942
}
943
944
/**
945
 * Make any theme changes that are sent with the profile.
946
 *
947
 * @param int $memID The ID of the user
948
 * @param int $id_theme The ID of the theme
949
 */
950
function makeThemeChanges($memID, $id_theme)
951
{
952
	global $modSettings, $smcFunc, $context, $user_info;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
953
954
	$reservedVars = array(
955
		'actual_theme_url',
956
		'actual_images_url',
957
		'base_theme_dir',
958
		'base_theme_url',
959
		'default_images_url',
960
		'default_theme_dir',
961
		'default_theme_url',
962
		'default_template',
963
		'images_url',
964
		'number_recent_posts',
965
		'smiley_sets_default',
966
		'theme_dir',
967
		'theme_id',
968
		'theme_layers',
969
		'theme_templates',
970
		'theme_url',
971
	);
972
973
	// Can't change reserved vars.
974
	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))
975
		fatal_lang_error('no_access', false);
976
977
	// Don't allow any overriding of custom fields with default or non-default options.
978
	$request = $smcFunc['db_query']('', '
979
		SELECT col_name
980
		FROM {db_prefix}custom_fields
981
		WHERE active = {int:is_active}',
982
		array(
983
			'is_active' => 1,
984
		)
985
	);
986
	$custom_fields = array();
987
	while ($row = $smcFunc['db_fetch_assoc']($request))
988
		$custom_fields[] = $row['col_name'];
989
	$smcFunc['db_free_result']($request);
990
991
	// These are the theme changes...
992
	$themeSetArray = array();
993
	if (isset($_POST['options']) && is_array($_POST['options']))
994
	{
995
		foreach ($_POST['options'] as $opt => $val)
996
		{
997
			if (in_array($opt, $custom_fields))
998
				continue;
999
1000
			// These need to be controlled.
1001
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1002
				$val = max(0, min($val, 50));
1003
			// We don't set this per theme anymore.
1004
			elseif ($opt == 'allow_no_censored')
1005
				continue;
1006
1007
			$themeSetArray[] = array($memID, $id_theme, $opt, is_array($val) ? implode(',', $val) : $val);
1008
		}
1009
	}
1010
1011
	$erase_options = array();
1012
	if (isset($_POST['default_options']) && is_array($_POST['default_options']))
1013
		foreach ($_POST['default_options'] as $opt => $val)
1014
		{
1015
			if (in_array($opt, $custom_fields))
1016
				continue;
1017
1018
			// These need to be controlled.
1019
			if ($opt == 'topics_per_page' || $opt == 'messages_per_page')
1020
				$val = max(0, min($val, 50));
1021
			// Only let admins and owners change the censor.
1022
			elseif ($opt == 'allow_no_censored' && !$user_info['is_admin'] && !$context['user']['is_owner'])
1023
					continue;
1024
1025
			$themeSetArray[] = array($memID, 1, $opt, is_array($val) ? implode(',', $val) : $val);
1026
			$erase_options[] = $opt;
1027
		}
1028
1029
	// If themeSetArray isn't still empty, send it to the database.
1030
	if (empty($context['password_auth_failed']))
1031
	{
1032 View Code Duplication
		if (!empty($themeSetArray))
1033
		{
1034
			$smcFunc['db_insert']('replace',
1035
				'{db_prefix}themes',
1036
				array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
1037
				$themeSetArray,
1038
				array('id_member', 'id_theme', 'variable')
1039
			);
1040
		}
1041
1042
		if (!empty($erase_options))
1043
		{
1044
			$smcFunc['db_query']('', '
1045
				DELETE FROM {db_prefix}themes
1046
				WHERE id_theme != {int:id_theme}
1047
					AND variable IN ({array_string:erase_variables})
1048
					AND id_member = {int:id_member}',
1049
				array(
1050
					'id_theme' => 1,
1051
					'id_member' => $memID,
1052
					'erase_variables' => $erase_options
1053
				)
1054
			);
1055
		}
1056
1057
		// Admins can choose any theme, even if it's not enabled...
1058
		$themes = allowedTo('admin_forum') ? explode(',', $modSettings['knownThemes']) : explode(',', $modSettings['enableThemes']);
1059
		foreach ($themes as $t)
1060
			cache_put_data('theme_settings-' . $t . ':' . $memID, null, 60);
1061
	}
1062
}
1063
1064
/**
1065
 * Make any notification changes that need to be made.
1066
 *
1067
 * @param int $memID The ID of the member
1068
 */
1069
function makeNotificationChanges($memID)
1070
{
1071
	global $smcFunc, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1072
1073
	require_once($sourcedir . '/Subs-Notify.php');
1074
1075
	// Update the boards they are being notified on.
1076
	if (isset($_POST['edit_notify_boards']) && !empty($_POST['notify_boards']))
1077
	{
1078
		// Make sure only integers are deleted.
1079
		foreach ($_POST['notify_boards'] as $index => $id)
1080
			$_POST['notify_boards'][$index] = (int) $id;
1081
1082
		// id_board = 0 is reserved for topic notifications.
1083
		$_POST['notify_boards'] = array_diff($_POST['notify_boards'], array(0));
1084
1085
		$smcFunc['db_query']('', '
1086
			DELETE FROM {db_prefix}log_notify
1087
			WHERE id_board IN ({array_int:board_list})
1088
				AND id_member = {int:selected_member}',
1089
			array(
1090
				'board_list' => $_POST['notify_boards'],
1091
				'selected_member' => $memID,
1092
			)
1093
		);
1094
	}
1095
1096
	// We are editing topic notifications......
1097
	elseif (isset($_POST['edit_notify_topics']) && !empty($_POST['notify_topics']))
1098
	{
1099
		foreach ($_POST['notify_topics'] as $index => $id)
1100
			$_POST['notify_topics'][$index] = (int) $id;
1101
1102
		// Make sure there are no zeros left.
1103
		$_POST['notify_topics'] = array_diff($_POST['notify_topics'], array(0));
1104
1105
		$smcFunc['db_query']('', '
1106
			DELETE FROM {db_prefix}log_notify
1107
			WHERE id_topic IN ({array_int:topic_list})
1108
				AND id_member = {int:selected_member}',
1109
			array(
1110
				'topic_list' => $_POST['notify_topics'],
1111
				'selected_member' => $memID,
1112
			)
1113
		);
1114
		foreach ($_POST['notify_topics'] as $topic)
1115
			setNotifyPrefs($memID, array('topic_notify_' . $topic => 0));
1116
	}
1117
1118
	// We are removing topic preferences
1119
	elseif (isset($_POST['remove_notify_topics']) && !empty($_POST['notify_topics']))
1120
	{
1121
		$prefs = array();
1122
		foreach ($_POST['notify_topics'] as $topic)
1123
			$prefs[] = 'topic_notify_' . $topic;
1124
		deleteNotifyPrefs($memID, $prefs);
1125
	}
1126
1127
	// We are removing board preferences
1128
	elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
1129
	{
1130
		$prefs = array();
1131
		foreach ($_POST['notify_boards'] as $board)
1132
			$prefs[] = 'board_notify_' . $board;
1133
		deleteNotifyPrefs($memID, $prefs);
1134
	}
1135
}
1136
1137
/**
1138
 * Save any changes to the custom profile fields
1139
 *
1140
 * @param int $memID The ID of the member
1141
 * @param string $area The area of the profile these fields are in
1142
 * @param bool $sanitize = true Whether or not to sanitize the data
1143
 * @param bool $returnErrors Whether or not to return any error information
1144
 * @return void|array Returns nothing or returns an array of error info if $returnErrors is true
1145
 */
1146
function makeCustomFieldChanges($memID, $area, $sanitize = true, $returnErrors = false)
1147
{
1148
	global $context, $smcFunc, $user_profile, $user_info, $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1149
	global $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1150
1151
	$errors = array();
1152
1153
	if ($sanitize && isset($_POST['customfield']))
1154
		$_POST['customfield'] = htmlspecialchars__recursive($_POST['customfield']);
1155
1156
	$where = $area == 'register' ? 'show_reg != 0' : 'show_profile = {string:area}';
1157
1158
	// Load the fields we are saving too - make sure we save valid data (etc).
1159
	$request = $smcFunc['db_query']('', '
1160
		SELECT col_name, field_name, field_desc, field_type, field_length, field_options, default_value, show_reg, mask, private
1161
		FROM {db_prefix}custom_fields
1162
		WHERE ' . $where . '
1163
			AND active = {int:is_active}',
1164
		array(
1165
			'is_active' => 1,
1166
			'area' => $area,
1167
		)
1168
	);
1169
	$changes = array();
1170
	$deletes = array();
1171
	$log_changes = array();
1172
	while ($row = $smcFunc['db_fetch_assoc']($request))
1173
	{
1174
		/* This means don't save if:
1175
			- The user is NOT an admin.
1176
			- The data is not freely viewable and editable by users.
1177
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
1178
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
1179
		*/
1180
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != $user_info['id'] || $row['private'] != 2) && ($area != 'register' || $row['show_reg'] == 0))
1181
			continue;
1182
1183
		// Validate the user data.
1184
		if ($row['field_type'] == 'check')
1185
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
1186
		elseif ($row['field_type'] == 'select' || $row['field_type'] == 'radio')
1187
		{
1188
			$value = $row['default_value'];
1189
			foreach (explode(',', $row['field_options']) as $k => $v)
1190
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
1191
					$value = $v;
1192
		}
1193
		// Otherwise some form of text!
1194
		else
1195
		{
1196
			$value = isset($_POST['customfield'][$row['col_name']]) ? $_POST['customfield'][$row['col_name']] : '';
1197
1198
			if ($row['field_length'])
1199
				$value = $smcFunc['substr']($value, 0, $row['field_length']);
1200
1201
			// Any masks?
1202
			if ($row['field_type'] == 'text' && !empty($row['mask']) && $row['mask'] != 'none')
1203
			{
1204
				$value = $smcFunc['htmltrim']($value);
1205
				$valueReference = un_htmlspecialchars($value);
1206
1207
				// Try and avoid some checks. '0' could be a valid non-empty value.
1208
				if (empty($value) && !is_numeric($value))
1209
					$value = '';
1210
1211
				if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $value != filter_var($value, FILTER_SANITIZE_STRING) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
1212
				{
1213
					if ($returnErrors)
1214
						$errors[] = 'custom_field_nohtml_fail';
1215
1216
					else
1217
						$value = '';
1218
				}
1219
				elseif ($row['mask'] == 'email' && (!filter_var($value, FILTER_VALIDATE_EMAIL) || strlen($value) > 255))
1220
				{
1221
					if ($returnErrors)
1222
						$errors[] = 'custom_field_mail_fail';
1223
1224
					else
1225
						$value = '';
1226
				}
1227
				elseif ($row['mask'] == 'number')
1228
				{
1229
					$value = (int) $value;
1230
				}
1231
				elseif (substr($row['mask'], 0, 5) == 'regex' && trim($value) != '' && preg_match(substr($row['mask'], 5), $value) === 0)
1232
				{
1233
					if ($returnErrors)
1234
						$errors[] = 'custom_field_regex_fail';
1235
1236
					else
1237
						$value = '';
1238
				}
1239
1240
				unset($valueReference);
1241
			}
1242
		}
1243
1244
		// Did it change?
1245
		if (!isset($user_profile[$memID]['options'][$row['col_name']]) || $user_profile[$memID]['options'][$row['col_name']] !== $value)
1246
		{
1247
			$log_changes[] = array(
1248
				'action' => 'customfield_' . $row['col_name'],
1249
				'log_type' => 'user',
1250
				'extra' => array(
1251
					'previous' => !empty($user_profile[$memID]['options'][$row['col_name']]) ? $user_profile[$memID]['options'][$row['col_name']] : '',
1252
					'new' => $value,
1253
					'applicator' => $user_info['id'],
1254
					'member_affected' => $memID,
1255
				),
1256
			);
1257
			if (empty($value))
1258
			{
1259
				$deletes = array('id_theme' => 1 , 'variable' => $row['col_name'], 'id_member' => $memID);
1260
				unset($user_profile[$memID]['options'][$row['col_name']]);
1261
			}
1262
			else
1263
			{
1264
				$changes[] = array(1, $row['col_name'], $value, $memID);
1265
				$user_profile[$memID]['options'][$row['col_name']] = $value;
1266
			}
1267
		}
1268
	}
1269
	$smcFunc['db_free_result']($request);
1270
1271
	$hook_errors = call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, &$errors, $returnErrors, $memID, $area, $sanitize, &$deletes));
1272
1273
	if (!empty($hook_errors) && is_array($hook_errors))
1274
		$errors = array_merge($errors, $hook_errors);
1275
1276
	// Make those changes!
1277
	if ((!empty($changes) || !empty($deletes)) && empty($context['password_auth_failed']) && empty($errors))
1278
	{
1279 View Code Duplication
		if (!empty($changes))
1280
			$smcFunc['db_insert']('replace',
1281
				'{db_prefix}themes',
1282
				array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
1283
				$changes,
1284
				array('id_theme', 'variable', 'id_member')
1285
			);
1286
		if (!empty($deletes))
1287
			$smcFunc['db_query']('','
1288
				DELETE FROM {db_prefix}themes
1289
				WHERE id_theme = {int:id_theme} AND
1290
						variable = {string:variable} AND
1291
						id_member = {int:id_member}',
1292
				$deletes
1293
				);
1294
		if (!empty($log_changes) && !empty($modSettings['modlog_enabled']))
1295
		{
1296
			require_once($sourcedir . '/Logging.php');
1297
			logActions($log_changes);
1298
		}
1299
	}
1300
1301
	if ($returnErrors)
1302
		return $errors;
1303
}
1304
1305
/**
1306
 * Show all the users buddies, as well as a add/delete interface.
1307
 *
1308
 * @param int $memID The ID of the member
1309
 */
1310
function editBuddyIgnoreLists($memID)
1311
{
1312
	global $context, $txt, $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1313
1314
	// Do a quick check to ensure people aren't getting here illegally!
1315
	if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
1316
		fatal_lang_error('no_access', false);
1317
1318
	// Can we email the user direct?
1319
	$context['can_moderate_forum'] = allowedTo('moderate_forum');
1320
	$context['can_send_email'] = allowedTo('moderate_forum');
1321
1322
	$subActions = array(
1323
		'buddies' => array('editBuddies', $txt['editBuddies']),
1324
		'ignore' => array('editIgnoreList', $txt['editIgnoreList']),
1325
	);
1326
1327
	$context['list_area'] = isset($_GET['sa']) && isset($subActions[$_GET['sa']]) ? $_GET['sa'] : 'buddies';
1328
1329
	// Create the tabs for the template.
1330
	$context[$context['profile_menu_name']]['tab_data'] = array(
1331
		'title' => $txt['editBuddyIgnoreLists'],
1332
		'description' => $txt['buddy_ignore_desc'],
1333
		'icon' => 'profile_hd.png',
1334
		'tabs' => array(
1335
			'buddies' => array(),
1336
			'ignore' => array(),
1337
		),
1338
	);
1339
1340
	loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
1341
1342
	// Pass on to the actual function.
1343
	$context['sub_template'] = $subActions[$context['list_area']][0];
1344
	$call = call_helper($subActions[$context['list_area']][0], true);
1345
1346
	if (!empty($call))
1347
		call_user_func($call, $memID);
1348
}
1349
1350
/**
1351
 * Show all the users buddies, as well as a add/delete interface.
1352
 *
1353
 * @param int $memID The ID of the member
1354
 */
1355
function editBuddies($memID)
1356
{
1357
	global $txt, $scripturl, $settings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1358
	global $context, $user_profile, $memberContext, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1359
1360
	// For making changes!
1361
	$buddiesArray = explode(',', $user_profile[$memID]['buddy_list']);
1362
	foreach ($buddiesArray as $k => $dummy)
1363
		if ($dummy == '')
1364
			unset($buddiesArray[$k]);
1365
1366
	// Removing a buddy?
1367
	if (isset($_GET['remove']))
1368
	{
1369
		checkSession('get');
1370
1371
		call_integration_hook('integrate_remove_buddy', array($memID));
1372
1373
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1374
1375
		// Heh, I'm lazy, do it the easy way...
1376
		foreach ($buddiesArray as $key => $buddy)
1377
			if ($buddy == (int) $_GET['remove'])
1378
			{
1379
				unset($buddiesArray[$key]);
1380
				$_SESSION['prf-save'] = true;
1381
			}
1382
1383
		// Make the changes.
1384
		$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1385
		updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1386
1387
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1388
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1389
	}
1390
	elseif (isset($_POST['new_buddy']))
1391
	{
1392
		checkSession();
1393
1394
		// Prepare the string for extraction...
1395
		$_POST['new_buddy'] = strtr($smcFunc['htmlspecialchars']($_POST['new_buddy'], ENT_QUOTES), array('&quot;' => '"'));
1396
		preg_match_all('~"([^"]+)"~', $_POST['new_buddy'], $matches);
1397
		$new_buddies = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_buddy']))));
1398
1399 View Code Duplication
		foreach ($new_buddies as $k => $dummy)
1400
		{
1401
			$new_buddies[$k] = strtr(trim($new_buddies[$k]), array('\'' => '&#039;'));
1402
1403
			if (strlen($new_buddies[$k]) == 0 || in_array($new_buddies[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1404
				unset($new_buddies[$k]);
1405
		}
1406
1407
		call_integration_hook('integrate_add_buddies', array($memID, &$new_buddies));
1408
1409
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1410 View Code Duplication
		if (!empty($new_buddies))
1411
		{
1412
			// Now find out the id_member of the buddy.
1413
			$request = $smcFunc['db_query']('', '
1414
				SELECT id_member
1415
				FROM {db_prefix}members
1416
				WHERE member_name IN ({array_string:new_buddies}) OR real_name IN ({array_string:new_buddies})
1417
				LIMIT {int:count_new_buddies}',
1418
				array(
1419
					'new_buddies' => $new_buddies,
1420
					'count_new_buddies' => count($new_buddies),
1421
				)
1422
			);
1423
1424
			if ($smcFunc['db_num_rows']($request) != 0)
1425
				$_SESSION['prf-save'] = true;
1426
1427
			// Add the new member to the buddies array.
1428
			while ($row = $smcFunc['db_fetch_assoc']($request))
1429
			{
1430
				if (in_array($row['id_member'], $buddiesArray))
1431
					continue;
1432
				else
1433
					$buddiesArray[] = (int) $row['id_member'];
1434
			}
1435
			$smcFunc['db_free_result']($request);
1436
1437
			// Now update the current users buddy list.
1438
			$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1439
			updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1440
		}
1441
1442
		// Back to the buddy list!
1443
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1444
	}
1445
1446
	// Get all the users "buddies"...
1447
	$buddies = array();
1448
1449
	// Gotta load the custom profile fields names.
1450
	$request = $smcFunc['db_query']('', '
1451
		SELECT col_name, field_name, field_desc, field_type, bbc, enclose
1452
		FROM {db_prefix}custom_fields
1453
		WHERE active = {int:active}
1454
			AND private < {int:private_level}',
1455
		array(
1456
			'active' => 1,
1457
			'private_level' => 2,
1458
		)
1459
	);
1460
1461
	$context['custom_pf'] = array();
1462
	$disabled_fields = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
0 ignored issues
show
Bug introduced by
The variable $modSettings does not exist. Did you mean $settings?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1463
	while ($row = $smcFunc['db_fetch_assoc']($request))
1464
		if (!isset($disabled_fields[$row['col_name']]))
1465
			$context['custom_pf'][$row['col_name']] = array(
1466
				'label' => $row['field_name'],
1467
				'type' => $row['field_type'],
1468
				'bbc' => !empty($row['bbc']),
1469
				'enclose' => $row['enclose'],
1470
			);
1471
1472
	// Gotta disable the gender option.
1473
	if (isset($context['custom_pf']['cust_gender']) && $context['custom_pf']['cust_gender'] == 'None')
1474
		unset($context['custom_pf']['cust_gender']);
1475
1476
	$smcFunc['db_free_result']($request);
1477
1478 View Code Duplication
	if (!empty($buddiesArray))
1479
	{
1480
		$result = $smcFunc['db_query']('', '
1481
			SELECT id_member
1482
			FROM {db_prefix}members
1483
			WHERE id_member IN ({array_int:buddy_list})
1484
			ORDER BY real_name
1485
			LIMIT {int:buddy_list_count}',
1486
			array(
1487
				'buddy_list' => $buddiesArray,
1488
				'buddy_list_count' => substr_count($user_profile[$memID]['buddy_list'], ',') + 1,
1489
			)
1490
		);
1491
		while ($row = $smcFunc['db_fetch_assoc']($result))
1492
			$buddies[] = $row['id_member'];
1493
		$smcFunc['db_free_result']($result);
1494
	}
1495
1496
	$context['buddy_count'] = count($buddies);
1497
1498
	// Load all the members up.
1499
	loadMemberData($buddies, false, 'profile');
1500
1501
	// Setup the context for each buddy.
1502
	$context['buddies'] = array();
1503
	foreach ($buddies as $buddy)
1504
	{
1505
		loadMemberContext($buddy);
1506
		$context['buddies'][$buddy] = $memberContext[$buddy];
1507
1508
		// Make sure to load the appropriate fields for each user
1509
		if (!empty($context['custom_pf']))
1510
		{
1511
			foreach ($context['custom_pf'] as $key => $column)
0 ignored issues
show
Bug introduced by
The expression $context['custom_pf'] of type array|integer is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1512
			{
1513
				// Don't show anything if there isn't anything to show.
1514 View Code Duplication
				if (!isset($context['buddies'][$buddy]['options'][$key]))
1515
				{
1516
					$context['buddies'][$buddy]['options'][$key] = '';
1517
					continue;
1518
				}
1519
1520 View Code Duplication
				if ($column['bbc'] && !empty($context['buddies'][$buddy]['options'][$key]))
1521
					$context['buddies'][$buddy]['options'][$key] = strip_tags(parse_bbc($context['buddies'][$buddy]['options'][$key]));
1522
1523
				elseif ($column['type'] == 'check')
1524
					$context['buddies'][$buddy]['options'][$key] = $context['buddies'][$buddy]['options'][$key] == 0 ? $txt['no'] : $txt['yes'];
1525
1526
				// Enclosing the user input within some other text?
1527
				if (!empty($column['enclose']) && !empty($context['buddies'][$buddy]['options'][$key]))
1528
					$context['buddies'][$buddy]['options'][$key] = strtr($column['enclose'], array(
1529
						'{SCRIPTURL}' => $scripturl,
1530
						'{IMAGES_URL}' => $settings['images_url'],
1531
						'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1532
						'{INPUT}' => $context['buddies'][$buddy]['options'][$key],
1533
					));
1534
			}
1535
		}
1536
	}
1537
1538 View Code Duplication
	if (isset($_SESSION['prf-save']))
1539
	{
1540
		if ($_SESSION['prf-save'] === true)
1541
			$context['saved_successful'] = true;
1542
		else
1543
			$context['saved_failed'] = $_SESSION['prf-save'];
1544
1545
		unset($_SESSION['prf-save']);
1546
	}
1547
1548
	call_integration_hook('integrate_view_buddies', array($memID));
1549
}
1550
1551
/**
1552
 * Allows the user to view their ignore list, as well as the option to manage members on it.
1553
 *
1554
 * @param int $memID The ID of the member
1555
 */
1556
function editIgnoreList($memID)
1557
{
1558
	global $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1559
	global $context, $user_profile, $memberContext, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1560
1561
	// For making changes!
1562
	$ignoreArray = explode(',', $user_profile[$memID]['pm_ignore_list']);
1563
	foreach ($ignoreArray as $k => $dummy)
1564
		if ($dummy == '')
1565
			unset($ignoreArray[$k]);
1566
1567
	// Removing a member from the ignore list?
1568
	if (isset($_GET['remove']))
1569
	{
1570
		checkSession('get');
1571
1572
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1573
1574
		// Heh, I'm lazy, do it the easy way...
1575
		foreach ($ignoreArray as $key => $id_remove)
1576
			if ($id_remove == (int) $_GET['remove'])
1577
			{
1578
				unset($ignoreArray[$key]);
1579
				$_SESSION['prf-save'] = true;
1580
			}
1581
1582
		// Make the changes.
1583
		$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1584
		updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1585
1586
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1587
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1588
	}
1589
	elseif (isset($_POST['new_ignore']))
1590
	{
1591
		checkSession();
1592
		// Prepare the string for extraction...
1593
		$_POST['new_ignore'] = strtr($smcFunc['htmlspecialchars']($_POST['new_ignore'], ENT_QUOTES), array('&quot;' => '"'));
1594
		preg_match_all('~"([^"]+)"~', $_POST['new_ignore'], $matches);
1595
		$new_entries = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_ignore']))));
1596
1597 View Code Duplication
		foreach ($new_entries as $k => $dummy)
1598
		{
1599
			$new_entries[$k] = strtr(trim($new_entries[$k]), array('\'' => '&#039;'));
1600
1601
			if (strlen($new_entries[$k]) == 0 || in_array($new_entries[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1602
				unset($new_entries[$k]);
1603
		}
1604
1605
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1606 View Code Duplication
		if (!empty($new_entries))
1607
		{
1608
			// Now find out the id_member for the members in question.
1609
			$request = $smcFunc['db_query']('', '
1610
				SELECT id_member
1611
				FROM {db_prefix}members
1612
				WHERE member_name IN ({array_string:new_entries}) OR real_name IN ({array_string:new_entries})
1613
				LIMIT {int:count_new_entries}',
1614
				array(
1615
					'new_entries' => $new_entries,
1616
					'count_new_entries' => count($new_entries),
1617
				)
1618
			);
1619
1620
			if ($smcFunc['db_num_rows']($request) != 0)
1621
				$_SESSION['prf-save'] = true;
1622
1623
			// Add the new member to the buddies array.
1624
			while ($row = $smcFunc['db_fetch_assoc']($request))
1625
			{
1626
				if (in_array($row['id_member'], $ignoreArray))
1627
					continue;
1628
				else
1629
					$ignoreArray[] = (int) $row['id_member'];
1630
			}
1631
			$smcFunc['db_free_result']($request);
1632
1633
			// Now update the current users buddy list.
1634
			$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1635
			updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1636
		}
1637
1638
		// Back to the list of pityful people!
1639
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1640
	}
1641
1642
	// Initialise the list of members we're ignoring.
1643
	$ignored = array();
1644
1645 View Code Duplication
	if (!empty($ignoreArray))
1646
	{
1647
		$result = $smcFunc['db_query']('', '
1648
			SELECT id_member
1649
			FROM {db_prefix}members
1650
			WHERE id_member IN ({array_int:ignore_list})
1651
			ORDER BY real_name
1652
			LIMIT {int:ignore_list_count}',
1653
			array(
1654
				'ignore_list' => $ignoreArray,
1655
				'ignore_list_count' => substr_count($user_profile[$memID]['pm_ignore_list'], ',') + 1,
1656
			)
1657
		);
1658
		while ($row = $smcFunc['db_fetch_assoc']($result))
1659
			$ignored[] = $row['id_member'];
1660
		$smcFunc['db_free_result']($result);
1661
	}
1662
1663
	$context['ignore_count'] = count($ignored);
1664
1665
	// Load all the members up.
1666
	loadMemberData($ignored, false, 'profile');
1667
1668
	// Setup the context for each buddy.
1669
	$context['ignore_list'] = array();
1670
	foreach ($ignored as $ignore_member)
1671
	{
1672
		loadMemberContext($ignore_member);
1673
		$context['ignore_list'][$ignore_member] = $memberContext[$ignore_member];
1674
	}
1675
1676 View Code Duplication
	if (isset($_SESSION['prf-save']))
1677
	{
1678
		if ($_SESSION['prf-save'] === true)
1679
			$context['saved_successful'] = true;
1680
		else
1681
			$context['saved_failed'] = $_SESSION['prf-save'];
1682
1683
		unset($_SESSION['prf-save']);
1684
	}
1685
}
1686
1687
/**
1688
 * Handles the account section of the profile
1689
 *
1690
 * @param int $memID The ID of the member
1691
 */
1692
function account($memID)
1693
{
1694
	global $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1695
1696
	loadThemeOptions($memID);
1697
	if (allowedTo(array('profile_identity_own', 'profile_identity_any', 'profile_password_own', 'profile_password_any')))
1698
		loadCustomFields($memID, 'account');
1699
1700
	$context['sub_template'] = 'edit_options';
1701
	$context['page_desc'] = $txt['account_info'];
1702
1703
	setupProfileContext(
1704
		array(
1705
			'member_name', 'real_name', 'date_registered', 'posts', 'lngfile', 'hr',
1706
			'id_group', 'hr',
1707
			'email_address', 'show_online', 'hr',
1708
			'tfa', 'hr',
1709
			'passwrd1', 'passwrd2', 'hr',
1710
			'secret_question', 'secret_answer',
1711
		)
1712
	);
1713
}
1714
1715
/**
1716
 * Handles the main "Forum Profile" section of the profile
1717
 *
1718
 * @param int $memID The ID of the member
1719
 */
1720
function forumProfile($memID)
1721
{
1722
	global $context, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1723
1724
	loadThemeOptions($memID);
1725
	if (allowedTo(array('profile_forum_own', 'profile_forum_any')))
1726
		loadCustomFields($memID, 'forumprofile');
1727
1728
	$context['sub_template'] = 'edit_options';
1729
	$context['page_desc'] = $txt['forumProfile_info'];
1730
	$context['show_preview_button'] = true;
1731
1732
	setupProfileContext(
1733
		array(
1734
			'avatar_choice', 'hr', 'personal_text', 'hr',
1735
			'bday1', 'usertitle', 'signature', 'hr',
1736
			'website_title', 'website_url',
1737
		)
1738
	);
1739
}
1740
1741
/**
1742
 * Recursive function to retrieve server-stored avatar files
1743
 *
1744
 * @param string $directory The directory to look for files in
1745
 * @param int $level How many levels we should go in the directory
1746
 * @return array An array of information about the files and directories found
1747
 */
1748
function getAvatars($directory, $level)
1749
{
1750
	global $context, $txt, $modSettings, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1751
1752
	$result = array();
1753
1754
	// Open the directory..
1755
	$dir = dir($modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory);
1756
	$dirs = array();
1757
	$files = array();
1758
1759
	if (!$dir)
1760
		return array();
1761
1762
	while ($line = $dir->read())
1763
	{
1764
		if (in_array($line, array('.', '..', 'blank.png', 'index.php')))
1765
			continue;
1766
1767
		if (is_dir($modSettings['avatar_directory'] . '/' . $directory . (!empty($directory) ? '/' : '') . $line))
1768
			$dirs[] = $line;
1769
		else
1770
			$files[] = $line;
1771
	}
1772
	$dir->close();
1773
1774
	// Sort the results...
1775
	natcasesort($dirs);
1776
	natcasesort($files);
1777
1778
	if ($level == 0)
1779
	{
1780
		$result[] = array(
1781
			'filename' => 'blank.png',
1782
			'checked' => in_array($context['member']['avatar']['server_pic'], array('', 'blank.png')),
1783
			'name' => $txt['no_pic'],
1784
			'is_dir' => false
1785
		);
1786
	}
1787
1788
	foreach ($dirs as $line)
1789
	{
1790
		$tmp = getAvatars($directory . (!empty($directory) ? '/' : '') . $line, $level + 1);
1791
		if (!empty($tmp))
1792
			$result[] = array(
1793
				'filename' => $smcFunc['htmlspecialchars']($line),
1794
				'checked' => strpos($context['member']['avatar']['server_pic'], $line . '/') !== false,
1795
				'name' => '[' . $smcFunc['htmlspecialchars'](str_replace('_', ' ', $line)) . ']',
1796
				'is_dir' => true,
1797
				'files' => $tmp
1798
		);
1799
		unset($tmp);
1800
	}
1801
1802
	foreach ($files as $line)
1803
	{
1804
		$filename = substr($line, 0, (strlen($line) - strlen(strrchr($line, '.'))));
1805
		$extension = substr(strrchr($line, '.'), 1);
1806
1807
		// Make sure it is an image.
1808
		if (strcasecmp($extension, 'gif') != 0 && strcasecmp($extension, 'jpg') != 0 && strcasecmp($extension, 'jpeg') != 0 && strcasecmp($extension, 'png') != 0 && strcasecmp($extension, 'bmp') != 0)
1809
			continue;
1810
1811
		$result[] = array(
1812
			'filename' => $smcFunc['htmlspecialchars']($line),
1813
			'checked' => $line == $context['member']['avatar']['server_pic'],
1814
			'name' => $smcFunc['htmlspecialchars'](str_replace('_', ' ', $filename)),
1815
			'is_dir' => false
1816
		);
1817
		if ($level == 1)
1818
			$context['avatar_list'][] = $directory . '/' . $line;
1819
	}
1820
1821
	return $result;
1822
}
1823
1824
/**
1825
 * Handles the "Look and Layout" section of the profile
1826
 *
1827
 * @param int $memID The ID of the member
1828
 */
1829
function theme($memID)
1830
{
1831
	global $txt, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1832
1833
	loadTemplate('Settings');
1834
	loadSubTemplate('options');
1835
1836
	// Let mods hook into the theme options.
1837
	call_integration_hook('integrate_theme_options');
1838
1839
	loadThemeOptions($memID);
1840
	if (allowedTo(array('profile_extra_own', 'profile_extra_any')))
1841
		loadCustomFields($memID, 'theme');
1842
1843
	$context['sub_template'] = 'edit_options';
1844
	$context['page_desc'] = $txt['theme_info'];
1845
1846
	setupProfileContext(
1847
		array(
1848
			'id_theme', 'smiley_set', 'hr',
1849
			'time_format', 'timezone', 'hr',
1850
			'theme_settings',
1851
		)
1852
	);
1853
}
1854
1855
/**
1856
 * Display the notifications and settings for changes.
1857
 *
1858
 * @param int $memID The ID of the member
1859
 */
1860
function notification($memID)
1861
{
1862
	global $txt, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1863
1864
	// Going to want this for consistency.
1865
	loadCSSFile('admin.css', array(), 'smf_admin');
1866
1867
	// This is just a bootstrap for everything else.
1868
	$sa = array(
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $sa. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1869
		'alerts' => 'alert_configuration',
1870
		'markread' => 'alert_markread',
1871
		'topics' => 'alert_notifications_topics',
1872
		'boards' => 'alert_notifications_boards',
1873
	);
1874
1875
	$subAction = !empty($_GET['sa']) && isset($sa[$_GET['sa']]) ? $_GET['sa'] : 'alerts';
1876
1877
	$context['sub_template'] = $sa[$subAction];
1878
	$context[$context['profile_menu_name']]['tab_data'] = array(
1879
		'title' => $txt['notification'],
1880
		'help' => '',
1881
		'description' => $txt['notification_info'],
1882
	);
1883
	$sa[$subAction]($memID);
1884
}
1885
1886
/**
1887
 * Handles configuration of alert preferences
1888
 *
1889
 * @param int $memID The ID of the member
1890
 */
1891
function alert_configuration($memID)
1892
{
1893
	global $txt, $context, $modSettings, $smcFunc, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1894
1895
	if (!isset($context['token_check']))
1896
		$context['token_check'] = 'profile-nt' . $memID;
1897
1898
	is_not_guest();
1899
	if (!$context['user']['is_owner'])
1900
		isAllowedTo('profile_extra_any');
1901
1902
	// Set the post action if we're coming from the profile...
1903
	if (!isset($context['action']))
1904
		$context['action'] = 'action=profile;area=notification;sa=alerts;u=' . $memID;
1905
1906
	// What options are set
1907
	loadThemeOptions($memID);
1908
	loadJavaScriptFile('alertSettings.js', array('minimize' => true), 'smf_alertSettings');
1909
1910
	// Now load all the values for this user.
1911
	require_once($sourcedir . '/Subs-Notify.php');
1912
	$prefs = getNotifyPrefs($memID, '', $memID != 0);
1913
1914
	$context['alert_prefs'] = !empty($prefs[$memID]) ? $prefs[$memID] : array();
1915
1916
	$context['member'] += array(
1917
		'alert_timeout' => isset($context['alert_prefs']['alert_timeout']) ? $context['alert_prefs']['alert_timeout'] : 10,
1918
		'notify_announcements' => isset($context['alert_prefs']['announcements']) ? $context['alert_prefs']['announcements'] : 0,
1919
	);
1920
1921
	// Now for the exciting stuff.
1922
	// We have groups of items, each item has both an alert and an email key as well as an optional help string.
1923
	// Valid values for these keys are 'always', 'yes', 'never'; if using always or never you should add a help string.
1924
	$alert_types = array(
1925
		'board' => array(
1926
			'topic_notify' => array('alert' => 'yes', 'email' => 'yes'),
1927
			'board_notify' => array('alert' => 'yes', 'email' => 'yes'),
1928
		),
1929
		'msg' => array(
1930
			'msg_mention' => array('alert' => 'yes', 'email' => 'yes'),
1931
			'msg_quote' => array('alert' => 'yes', 'email' => 'yes'),
1932
			'msg_like' => array('alert' => 'yes', 'email' => 'never'),
1933
			'unapproved_reply' => array('alert' => 'yes', 'email' => 'yes'),
1934
		),
1935
		'pm' => array(
1936
			'pm_new' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_read', 'is_board' => false)),
1937
			'pm_reply' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_send', 'is_board' => false)),
1938
		),
1939
		'groupr' => array(
1940
			'groupr_approved' => array('alert' => 'always', 'email' => 'yes'),
1941
			'groupr_rejected' => array('alert' => 'always', 'email' => 'yes'),
1942
		),
1943
		'moderation' => array(
1944
			'unapproved_post' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'approve_posts', 'is_board' => true)),
1945
			'msg_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1946
			'msg_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1947
			'member_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1948
			'member_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1949
		),
1950
		'members' => array(
1951
			'member_register' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1952
			'request_group' => array('alert' => 'yes', 'email' => 'yes'),
1953
			'warn_any' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'issue_warning', 'is_board' => false)),
1954
			'buddy_request'  => array('alert' => 'yes', 'email' => 'never'),
1955
			'birthday'  => array('alert' => 'yes', 'email' => 'yes'),
1956
		),
1957
		'calendar' => array(
1958
			'event_new' => array('alert' => 'yes', 'email' => 'yes', 'help' => 'alert_event_new'),
1959
		),
1960
		'paidsubs' => array(
1961
			'paidsubs_expiring' => array('alert' => 'yes', 'email' => 'yes'),
1962
		),
1963
	);
1964
	$group_options = array(
1965
		'board' => array(
1966
			array('check', 'msg_auto_notify', 'label' => 'after'),
1967
			array('check', 'msg_receive_body', 'label' => 'after'),
1968
			array('select', 'msg_notify_pref', 'label' => 'before', 'opts' => array(
1969
				0 => $txt['alert_opt_msg_notify_pref_nothing'],
1970
				1 => $txt['alert_opt_msg_notify_pref_instant'],
1971
				2 => $txt['alert_opt_msg_notify_pref_first'],
1972
				3 => $txt['alert_opt_msg_notify_pref_daily'],
1973
				4 => $txt['alert_opt_msg_notify_pref_weekly'],
1974
			)),
1975
			array('select', 'msg_notify_type', 'label' => 'before', 'opts' => array(
1976
				1 => $txt['notify_send_type_everything'],
1977
				2 => $txt['notify_send_type_everything_own'],
1978
				3 => $txt['notify_send_type_only_replies'],
1979
				4 => $txt['notify_send_type_nothing'],
1980
			)),
1981
		),
1982
		'pm' => array(
1983
			array('select', 'pm_notify', 'label' => 'before', 'opts' => array(
1984
				1 => $txt['email_notify_all'],
1985
				2 => $txt['email_notify_buddies'],
1986
			)),
1987
		),
1988
	);
1989
1990
	// There are certain things that are disabled at the group level.
1991
	if (empty($modSettings['cal_enabled']))
1992
		unset($alert_types['calendar']);
1993
1994
	// Disable paid subscriptions at group level if they're disabled
1995
	if (empty($modSettings['paid_enabled']))
1996
		unset($alert_types['paidsubs']);
1997
1998
	// Disable membergroup requests at group level if they're disabled
1999
	if (empty($modSettings['show_group_membership']))
2000
		unset($alert_types['groupr'], $alert_types['members']['request_group']);
2001
2002
	// Disable mentions if they're disabled
2003
	if (empty($modSettings['enable_mentions']))
2004
		unset($alert_types['msg']['msg_mention']);
2005
2006
	// Disable likes if they're disabled
2007
	if (empty($modSettings['enable_likes']))
2008
		unset($alert_types['msg']['msg_like']);
2009
2010
	// Disable buddy requests if they're disabled
2011
	if (empty($modSettings['enable_buddylist']))
2012
		unset($alert_types['members']['buddy_request']);
2013
2014
	// Now, now, we could pass this through global but we should really get into the habit of
2015
	// passing content to hooks, not expecting hooks to splatter everything everywhere.
2016
	call_integration_hook('integrate_alert_types', array(&$alert_types, &$group_options));
2017
2018
	// Now we have to do some permissions testing - but only if we're not loading this from the admin center
2019
	if (!empty($memID))
2020
	{
2021
		require_once($sourcedir . '/Subs-Members.php');
2022
		$perms_cache = array();
2023
		$request = $smcFunc['db_query']('', '
2024
			SELECT COUNT(*)
2025
			FROM {db_prefix}group_moderators
2026
			WHERE id_member = {int:memID}',
2027
			array(
2028
				'memID' => $memID,
2029
			)
2030
		);
2031
2032
		list ($can_mod) = $smcFunc['db_fetch_row']($request);
2033
2034
		if (!isset($perms_cache['manage_membergroups']))
2035
		{
2036
			$members = membersAllowedTo('manage_membergroups');
2037
			$perms_cache['manage_membergroups'] = in_array($memID, $members);
2038
		}
2039
2040
		if (!($perms_cache['manage_membergroups'] || $can_mod != 0))
2041
			unset($alert_types['members']['request_group']);
2042
2043
		foreach ($alert_types as $group => $items)
2044
		{
2045
			foreach ($items as $alert_key => $alert_value)
2046
			{
2047
				if (!isset($alert_value['permission']))
2048
					continue;
2049
				if (!isset($perms_cache[$alert_value['permission']['name']]))
2050
				{
2051
					$in_board = !empty($alert_value['permission']['is_board']) ? 0 : null;
2052
					$members = membersAllowedTo($alert_value['permission']['name'], $in_board);
2053
					$perms_cache[$alert_value['permission']['name']] = in_array($memID, $members);
2054
				}
2055
2056
				if (!$perms_cache[$alert_value['permission']['name']])
2057
					unset ($alert_types[$group][$alert_key]);
2058
			}
2059
2060
			if (empty($alert_types[$group]))
2061
				unset ($alert_types[$group]);
2062
		}
2063
	}
2064
2065
	// And finally, exporting it to be useful later.
2066
	$context['alert_types'] = $alert_types;
2067
	$context['alert_group_options'] = $group_options;
2068
2069
	$context['alert_bits'] = array(
2070
		'alert' => 0x01,
2071
		'email' => 0x02,
2072
	);
2073
2074
	if (isset($_POST['notify_submit']))
2075
	{
2076
		checkSession();
2077
		validateToken($context['token_check'], 'post');
2078
2079
		// We need to step through the list of valid settings and figure out what the user has set.
2080
		$update_prefs = array();
2081
2082
		// Now the group level options
2083
		foreach ($context['alert_group_options'] as $opt_group => $group)
2084
		{
2085
			foreach ($group as $this_option)
2086
			{
2087
				switch ($this_option[0])
2088
				{
2089
					case 'check':
2090
						$update_prefs[$this_option[1]] = !empty($_POST['opt_' . $this_option[1]]) ? 1 : 0;
2091
						break;
2092
					case 'select':
2093
						if (isset($_POST['opt_' . $this_option[1]], $this_option['opts'][$_POST['opt_' . $this_option[1]]]))
2094
							$update_prefs[$this_option[1]] = $_POST['opt_' . $this_option[1]];
2095
						else
2096
						{
2097
							// We didn't have a sane value. Let's grab the first item from the possibles.
2098
							$keys = array_keys($this_option['opts']);
2099
							$first = array_shift($keys);
2100
							$update_prefs[$this_option[1]] = $first;
2101
						}
2102
						break;
2103
				}
2104
			}
2105
		}
2106
2107
		// Now the individual options
2108
		foreach ($context['alert_types'] as $alert_group => $items)
2109
		{
2110
			foreach ($items as $item_key => $this_options)
2111
			{
2112
				$this_value = 0;
2113
				foreach ($context['alert_bits'] as $type => $bitvalue)
2114
				{
2115
					if ($this_options[$type] == 'yes' && !empty($_POST[$type . '_' . $item_key]) || $this_options[$type] == 'always')
2116
						$this_value |= $bitvalue;
2117
				}
2118
				if (!isset($context['alert_prefs'][$item_key]) || $context['alert_prefs'][$item_key] != $this_value)
2119
					$update_prefs[$item_key] = $this_value;
2120
			}
2121
		}
2122
2123
		if (!empty($_POST['opt_alert_timeout']))
2124
			$update_prefs['alert_timeout'] = $context['member']['alert_timeout'] = (int) $_POST['opt_alert_timeout'];
2125
2126
		if (!empty($_POST['notify_announcements']))
2127
			$update_prefs['announcements'] = $context['member']['notify_announcements'] = (int) $_POST['notify_announcements'];
2128
2129
		setNotifyPrefs((int) $memID, $update_prefs);
2130
		foreach ($update_prefs as $pref => $value)
2131
			$context['alert_prefs'][$pref] = $value;
2132
2133
		makeNotificationChanges($memID);
2134
2135
		$context['profile_updated'] = $txt['profile_updated_own'];
2136
	}
2137
2138
	createToken($context['token_check'], 'post');
2139
}
2140
2141
/**
2142
 * Marks all alerts as read for the specified user
2143
 *
2144
 * @param int $memID The ID of the member
2145
 */
2146
function alert_markread($memID)
2147
{
2148
	global $context, $db_show_debug, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2149
2150
	// We do not want to output debug information here.
2151
	$db_show_debug = false;
2152
2153
	// We only want to output our little layer here.
2154
	$context['template_layers'] = array();
2155
	$context['sub_template'] = 'alerts_all_read';
2156
2157
	loadLanguage('Alerts');
2158
2159
	// Now we're all set up.
2160
	is_not_guest();
2161
	if (!$context['user']['is_owner'])
2162
		fatal_error('no_access');
2163
2164
	checkSession('get');
2165
2166
	// Assuming we're here, mark everything as read and head back.
2167
	// We only spit back the little layer because this should be called AJAXively.
2168
	$smcFunc['db_query']('', '
2169
		UPDATE {db_prefix}user_alerts
2170
		SET is_read = {int:now}
2171
		WHERE id_member = {int:current_member}
2172
			AND is_read = 0',
2173
		array(
2174
			'now' => time(),
2175
			'current_member' => $memID,
2176
		)
2177
	);
2178
2179
	updateMemberData($memID, array('alerts' => 0));
2180
}
2181
2182
/**
2183
 * Marks a group of alerts as un/read
2184
 *
2185
 * @param int $memID The user ID.
2186
 * @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.
2187
 * @param integer $read To mark as read or unread, 1 for read, 0 or any other value different than 1 for unread.
2188
 * @return integer How many alerts remain unread
0 ignored issues
show
Documentation introduced by
Should the return type not be false|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2189
 */
2190
function alert_mark($memID, $toMark, $read = 0)
2191
{
2192
	global $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2193
2194
	if (empty($toMark) || empty($memID))
2195
		return false;
2196
2197
	$toMark = (array) $toMark;
2198
2199
	$smcFunc['db_query']('', '
2200
		UPDATE {db_prefix}user_alerts
2201
		SET is_read = {int:read}
2202
		WHERE id_alert IN({array_int:toMark})',
2203
		array(
2204
			'read' => $read == 1 ? time() : 0,
2205
			'toMark' => $toMark,
2206
		)
2207
	);
2208
2209
	// Gotta know how many unread alerts are left.
2210
	$count = alert_count($memID, true);
2211
2212
	updateMemberData($memID, array('alerts' => $count));
2213
2214
	// Might want to know this.
2215
	return $count;
2216
}
2217
2218
/**
2219
 * Deletes a single or a group of alerts by ID
2220
 *
2221
 * @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.
2222
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2223
 * @return void|int If the $memID param is set, returns the new amount of unread alerts.
0 ignored issues
show
Documentation introduced by
Should the return type not be false|integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2224
 */
2225
function alert_delete($toDelete, $memID = false)
2226
{
2227
	global $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2228
2229
	if (empty($toDelete))
2230
		return false;
2231
2232
	$toDelete = (array) $toDelete;
2233
2234
	$smcFunc['db_query']('', '
2235
		DELETE FROM {db_prefix}user_alerts
2236
		WHERE id_alert IN({array_int:toDelete})',
2237
		array(
2238
			'toDelete' => $toDelete,
2239
		)
2240
	);
2241
2242
	// Gotta know how many unread alerts are left.
2243
	if ($memID)
2244
	{
2245
		$count = alert_count($memID, true);
0 ignored issues
show
Bug introduced by
It seems like $memID defined by parameter $memID on line 2225 can also be of type boolean; however, alert_count() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2246
2247
		updateMemberData($memID, array('alerts' => $count));
2248
2249
		// Might want to know this.
2250
		return $count;
2251
	}
2252
}
2253
2254
/**
2255
 * Counts how many alerts a user has - either unread or all depending on $unread
2256
 * We can't use db_num_rows here, as we have to determine what boards the user can see
2257
 * Possibly in future versions as database support for json is mainstream, we can simplify this.
2258
 *
2259
 * @param int $memID The user ID.
2260
 * @param bool $unread Whether to only count unread alerts.
2261
 * @return int The number of requested alerts
2262
 */
2263
function alert_count($memID, $unread = false)
2264
{
2265
	global $smcFunc, $user_info;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2266
2267
	if (empty($memID))
2268
		return false;
2269
2270
	// Leave a place holder here for now.
2271
	// MySQL uses JSON_CONTAINS() to find data, Postgresql seems to be able to natively use it with JSONB column type.
2272
	// Maybe a $smcFunc['db_json_supported']?
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2273
	if (false)
0 ignored issues
show
Bug introduced by
Avoid IF statements that are always true or false
Loading history...
2274
	{
2275
		$request = $smcFunc['db_query']('', '
2276
			SELECT id_alert
2277
			FROM {db_prefix}user_alerts
2278
			WHERE id_member = {int:id_member}
2279
				'.($unread ? '
2280
				AND is_read = 0' : ''),
2281
			array(
2282
				'id_member' => $memID,
2283
			)
2284
		);
2285
2286
		$count = $smcFunc['db_num_rows']($request);
2287
		$smcFunc['db_free_result']($request);
2288
2289
		return $count;
2290
	}
2291
2292
	// We have to do this the slow way as to iterate over all possible boards the user can see.
2293
	$request = $smcFunc['db_query']('', '
2294
		SELECT id_alert, extra
2295
		FROM {db_prefix}user_alerts
2296
		WHERE id_member = {int:id_member}
2297
			'.($unread ? '
2298
			AND is_read = 0' : ''),
2299
		array(
2300
			'id_member' => $memID,
2301
		)
2302
	);
2303
2304
	// First we dump alerts and possible boards information out.
2305
	$alerts = array();
2306
	$boards = array();
2307
	$possible_boards = array();
2308
	while ($row = $smcFunc['db_fetch_assoc']($request))
2309
	{
2310
		$alerts[$row['id_alert']] = !empty($row['extra']) ? $smcFunc['json_decode']($row['extra'], true) : array();
2311
2312
		// Only add to possible boards ones that are not empty and that we haven't set before.
2313
		if (!empty($alerts[$row['id_alert']]['board']) && !isset($possible_boards[$alerts[$row['id_alert']]['board']]))
2314
			$possible_boards[$alerts[$row['id_alert']]['board']] = $alerts[$row['id_alert']]['board'];
2315
	}
2316
	$smcFunc['db_free_result']($request);
2317
2318
	// If this isn't the current user, get their boards.
2319
	if (isset($user_info) && $user_info['id'] != $memID)
2320
	{
2321
		$query_see_board = build_query_board($memID);
2322
		$query_see_board = $query_see_board['query_see_board'];
2323
	}
2324
2325
	// Find only the boards they can see.
2326
	if (!empty($possible_boards))
2327
	{
2328
		$request = $smcFunc['db_query']('', '
2329
			SELECT id_board
2330
			FROM {db_prefix}boards AS b
2331
			WHERE ' . (!empty($query_see_board) ? '{raw:query_see_board}' : '{query_see_board}') . '
2332
				AND id_board IN ({array_int:boards})',
2333
			array(
2334
				'boards' => array_keys($possible_boards),
2335
				'query_see_board' => $query_see_board
0 ignored issues
show
Bug introduced by
The variable $query_see_board does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2336
			)
2337
		);
2338
		while ($row = $smcFunc['db_fetch_assoc']($request))
2339
			$boards[$row['id_board']] = $row['id_board'];
2340
	}
2341
	unset($possible_boards);
2342
2343
	// Now check alerts again and remove any they can't see.
2344
	foreach ($alerts as $id_alert => $extra)
2345
		if (!isset($boards[$extra['board']]))
2346
			unset($alerts[$id_alert]);		
2347
2348
	return count($alerts);
2349
}
2350
2351
/**
2352
 * Handles alerts related to topics and posts
2353
 *
2354
 * @param int $memID The ID of the member
2355
 */
2356
function alert_notifications_topics($memID)
2357
{
2358
	global $txt, $scripturl, $context, $modSettings, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2359
2360
	// Because of the way this stuff works, we want to do this ourselves.
2361 View Code Duplication
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
2362
	{
2363
		checkSession();
2364
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2365
2366
		makeNotificationChanges($memID);
2367
		$context['profile_updated'] = $txt['profile_updated_own'];
2368
	}
2369
2370
	// Now set up for the token check.
2371
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2372
	createToken($context['token_check'], 'post');
2373
2374
	// Gonna want this for the list.
2375
	require_once($sourcedir . '/Subs-List.php');
2376
2377
	// Do the topic notifications.
2378
	$listOptions = array(
2379
		'id' => 'topic_notification_list',
2380
		'width' => '100%',
2381
		'items_per_page' => $modSettings['defaultMaxListItems'],
2382
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2383
		'no_items_align' => 'left',
2384
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2385
		'default_sort_col' => 'last_post',
2386
		'get_items' => array(
2387
			'function' => 'list_getTopicNotifications',
2388
			'params' => array(
2389
				$memID,
2390
			),
2391
		),
2392
		'get_count' => array(
2393
			'function' => 'list_getTopicNotificationCount',
2394
			'params' => array(
2395
				$memID,
2396
			),
2397
		),
2398
		'columns' => array(
2399
			'subject' => array(
2400
				'header' => array(
2401
					'value' => $txt['notifications_topics'],
2402
					'class' => 'lefttext',
2403
				),
2404
				'data' => array(
2405
					'function' => function($topic) use ($txt)
2406
					{
2407
						$link = $topic['link'];
2408
2409
						if ($topic['new'])
2410
							$link .= ' <a href="' . $topic['new_href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2411
2412
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2413
2414
						return $link;
2415
					},
2416
				),
2417
				'sort' => array(
2418
					'default' => 'ms.subject',
2419
					'reverse' => 'ms.subject DESC',
2420
				),
2421
			),
2422
			'started_by' => array(
2423
				'header' => array(
2424
					'value' => $txt['started_by'],
2425
					'class' => 'lefttext',
2426
				),
2427
				'data' => array(
2428
					'db' => 'poster_link',
2429
				),
2430
				'sort' => array(
2431
					'default' => 'real_name_col',
2432
					'reverse' => 'real_name_col DESC',
2433
				),
2434
			),
2435
			'last_post' => array(
2436
				'header' => array(
2437
					'value' => $txt['last_post'],
2438
					'class' => 'lefttext',
2439
				),
2440
				'data' => array(
2441
					'sprintf' => array(
2442
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2443
						'params' => array(
2444
							'updated' => false,
2445
							'poster_updated_link' => false,
2446
						),
2447
					),
2448
				),
2449
				'sort' => array(
2450
					'default' => 'ml.id_msg DESC',
2451
					'reverse' => 'ml.id_msg',
2452
				),
2453
			),
2454
			'alert' => array(
2455
				'header' => array(
2456
					'value' => $txt['notify_what_how'],
2457
					'class' => 'lefttext',
2458
				),
2459
				'data' => array(
2460
					'function' => function($topic) use ($txt)
2461
					{
2462
						$pref = $topic['notify_pref'];
2463
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2464
						return $txt['notify_topic_' . $mode];
2465
					},
2466
				),
2467
			),
2468
			'delete' => array(
2469
				'header' => array(
2470
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2471
					'style' => 'width: 4%;',
2472
					'class' => 'centercol',
2473
				),
2474
				'data' => array(
2475
					'sprintf' => array(
2476
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d">',
2477
						'params' => array(
2478
							'id' => false,
2479
						),
2480
					),
2481
					'class' => 'centercol',
2482
				),
2483
			),
2484
		),
2485
		'form' => array(
2486
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2487
			'include_sort' => true,
2488
			'include_start' => true,
2489
			'hidden_fields' => array(
2490
				'u' => $memID,
2491
				'sa' => $context['menu_item_selected'],
2492
				$context['session_var'] => $context['session_id'],
2493
			),
2494
			'token' => $context['token_check'],
2495
		),
2496
		'additional_rows' => array(
2497
			array(
2498
				'position' => 'bottom_of_list',
2499
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button" />
2500
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2501
				'class' => 'floatright',
2502
			),
2503
		),
2504
	);
2505
2506
	// Create the notification list.
2507
	createList($listOptions);
2508
}
2509
2510
/**
2511
 * Handles preferences related to board-level notifications
2512
 *
2513
 * @param int $memID The ID of the member
2514
 */
2515
function alert_notifications_boards($memID)
2516
{
2517
	global $txt, $scripturl, $context, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2518
2519
	// Because of the way this stuff works, we want to do this ourselves.
2520 View Code Duplication
	if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
0 ignored issues
show
Bug introduced by
The variable $_POSt seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
2521
	{
2522
		checkSession();
2523
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2524
2525
		makeNotificationChanges($memID);
2526
		$context['profile_updated'] = $txt['profile_updated_own'];
2527
	}
2528
2529
	// Now set up for the token check.
2530
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2531
	createToken($context['token_check'], 'post');
2532
2533
	// Gonna want this for the list.
2534
	require_once($sourcedir . '/Subs-List.php');
2535
2536
	// Fine, start with the board list.
2537
	$listOptions = array(
2538
		'id' => 'board_notification_list',
2539
		'width' => '100%',
2540
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2541
		'no_items_align' => 'left',
2542
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2543
		'default_sort_col' => 'board_name',
2544
		'get_items' => array(
2545
			'function' => 'list_getBoardNotifications',
2546
			'params' => array(
2547
				$memID,
2548
			),
2549
		),
2550
		'columns' => array(
2551
			'board_name' => array(
2552
				'header' => array(
2553
					'value' => $txt['notifications_boards'],
2554
					'class' => 'lefttext',
2555
				),
2556
				'data' => array(
2557
					'function' => function($board) use ($txt)
2558
					{
2559
						$link = $board['link'];
2560
2561
						if ($board['new'])
2562
							$link .= ' <a href="' . $board['href'] . '" class="new_posts">' . $txt['new'] . '</a>';
2563
2564
						return $link;
2565
					},
2566
				),
2567
				'sort' => array(
2568
					'default' => 'name',
2569
					'reverse' => 'name DESC',
2570
				),
2571
			),
2572
			'alert' => array(
2573
				'header' => array(
2574
					'value' => $txt['notify_what_how'],
2575
					'class' => 'lefttext',
2576
				),
2577
				'data' => array(
2578
					'function' => function($board) use ($txt)
2579
					{
2580
						$pref = $board['notify_pref'];
2581
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2582
						return $txt['notify_board_' . $mode];
2583
					},
2584
				),
2585
			),
2586
			'delete' => array(
2587
				'header' => array(
2588
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2589
					'style' => 'width: 4%;',
2590
					'class' => 'centercol',
2591
				),
2592
				'data' => array(
2593
					'sprintf' => array(
2594
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d">',
2595
						'params' => array(
2596
							'id' => false,
2597
						),
2598
					),
2599
					'class' => 'centercol',
2600
				),
2601
			),
2602
		),
2603
		'form' => array(
2604
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2605
			'include_sort' => true,
2606
			'include_start' => true,
2607
			'hidden_fields' => array(
2608
				'u' => $memID,
2609
				'sa' => $context['menu_item_selected'],
2610
				$context['session_var'] => $context['session_id'],
2611
			),
2612
			'token' => $context['token_check'],
2613
		),
2614
		'additional_rows' => array(
2615
			array(
2616
				'position' => 'bottom_of_list',
2617
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button">
2618
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2619
				'class' => 'floatright',
2620
			),
2621
		),
2622
	);
2623
2624
	// Create the board notification list.
2625
	createList($listOptions);
2626
}
2627
2628
/**
2629
 * Determins how many topics a user has requested notifications for
2630
 *
2631
 * @param int $memID The ID of the member
2632
 * @return int The number of topic notifications for this user
2633
 */
2634
function list_getTopicNotificationCount($memID)
2635
{
2636
	global $smcFunc, $user_info, $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2637
2638
	$request = $smcFunc['db_query']('', '
2639
		SELECT COUNT(*)
2640
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2641
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2642
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2643
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2644
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2645
			AND t.approved = {int:is_approved}' : ''),
2646
		array(
2647
			'selected_member' => $memID,
2648
			'is_approved' => 1,
2649
		)
2650
	);
2651
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2652
	$smcFunc['db_free_result']($request);
2653
2654
	return (int) $totalNotifications;
2655
}
2656
2657
/**
2658
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2659
 *
2660
 * @param int $start Which item to start with (for pagination purposes)
2661
 * @param int $items_per_page How many items to display on each page
2662
 * @param string $sort A string indicating how to sort the results
2663
 * @param int $memID The ID of the member
2664
 * @return array An array of information about the topics a user has subscribed to
2665
 */
2666
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2667
{
2668
	global $smcFunc, $scripturl, $user_info, $modSettings, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2669
2670
	require_once($sourcedir . '/Subs-Notify.php');
2671
	$prefs = getNotifyPrefs($memID);
2672
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2673
2674
	// All the topics with notification on...
2675
	$request = $smcFunc['db_query']('', '
2676
		SELECT
2677
			COALESCE(lt.id_msg, COALESCE(lmr.id_msg, -1)) + 1 AS new_from, b.id_board, b.name,
2678
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2679
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2680
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2681
			lt.unwatched
2682
		FROM {db_prefix}log_notify AS ln
2683
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2684
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2685
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2686
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2687
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2688
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2689
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2690
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2691
		WHERE ln.id_member = {int:selected_member}
2692
		ORDER BY {raw:sort}
2693
		LIMIT {int:offset}, {int:items_per_page}',
2694
		array(
2695
			'current_member' => $user_info['id'],
2696
			'is_approved' => 1,
2697
			'selected_member' => $memID,
2698
			'sort' => $sort,
2699
			'offset' => $start,
2700
			'items_per_page' => $items_per_page,
2701
		)
2702
	);
2703
	$notification_topics = array();
2704
	while ($row = $smcFunc['db_fetch_assoc']($request))
2705
	{
2706
		censorText($row['subject']);
2707
2708
		$notification_topics[] = array(
2709
			'id' => $row['id_topic'],
2710
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2711
			'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>',
2712
			'subject' => $row['subject'],
2713
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2714
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2715
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2716
			'new_from' => $row['new_from'],
2717
			'updated' => timeformat($row['poster_time']),
2718
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2719
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2720
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2721
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2722
			'unwatched' => $row['unwatched'],
2723
		);
2724
	}
2725
	$smcFunc['db_free_result']($request);
2726
2727
	return $notification_topics;
2728
}
2729
2730
/**
2731
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2732
 *
2733
 * @param int $start Which item to start with (not used here)
2734
 * @param int $items_per_page How many items to show on each page (not used here)
2735
 * @param string $sort A string indicating how to sort the results
2736
 * @param int $memID The ID of the member
2737
 * @return array An array of information about all the boards a user is subscribed to
2738
 */
2739
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2740
{
2741
	global $smcFunc, $scripturl, $user_info, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2742
2743
	require_once($sourcedir . '/Subs-Notify.php');
2744
	$prefs = getNotifyPrefs($memID);
2745
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2746
2747
	$request = $smcFunc['db_query']('', '
2748
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2749
		FROM {db_prefix}log_notify AS ln
2750
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2751
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2752
		WHERE ln.id_member = {int:selected_member}
2753
			AND {query_see_board}
2754
		ORDER BY {raw:sort}',
2755
		array(
2756
			'current_member' => $user_info['id'],
2757
			'selected_member' => $memID,
2758
			'sort' => $sort,
2759
		)
2760
	);
2761
	$notification_boards = array();
2762
	while ($row = $smcFunc['db_fetch_assoc']($request))
2763
		$notification_boards[] = array(
2764
			'id' => $row['id_board'],
2765
			'name' => $row['name'],
2766
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2767
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2768
			'new' => $row['board_read'] < $row['id_msg_updated'],
2769
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2770
		);
2771
	$smcFunc['db_free_result']($request);
2772
2773
	return $notification_boards;
2774
}
2775
2776
/**
2777
 * Loads the theme options for a user
2778
 *
2779
 * @param int $memID The ID of the member
2780
 */
2781
function loadThemeOptions($memID)
2782
{
2783
	global $context, $options, $cur_profile, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2784
2785 View Code Duplication
	if (isset($_POST['default_options']))
2786
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2787
2788
	if ($context['user']['is_owner'])
2789
	{
2790
		$context['member']['options'] = $options;
2791
		if (isset($_POST['options']) && is_array($_POST['options']))
2792
			foreach ($_POST['options'] as $k => $v)
2793
				$context['member']['options'][$k] = $v;
2794
	}
2795
	else
2796
	{
2797
		$request = $smcFunc['db_query']('', '
2798
			SELECT id_member, variable, value
2799
			FROM {db_prefix}themes
2800
			WHERE id_theme IN (1, {int:member_theme})
2801
				AND id_member IN (-1, {int:selected_member})',
2802
			array(
2803
				'member_theme' => (int) $cur_profile['id_theme'],
2804
				'selected_member' => $memID,
2805
			)
2806
		);
2807
		$temp = array();
2808
		while ($row = $smcFunc['db_fetch_assoc']($request))
2809
		{
2810
			if ($row['id_member'] == -1)
2811
			{
2812
				$temp[$row['variable']] = $row['value'];
2813
				continue;
2814
			}
2815
2816
			if (isset($_POST['options'][$row['variable']]))
2817
				$row['value'] = $_POST['options'][$row['variable']];
2818
			$context['member']['options'][$row['variable']] = $row['value'];
2819
		}
2820
		$smcFunc['db_free_result']($request);
2821
2822
		// Load up the default theme options for any missing.
2823
		foreach ($temp as $k => $v)
2824
		{
2825
			if (!isset($context['member']['options'][$k]))
2826
				$context['member']['options'][$k] = $v;
2827
		}
2828
	}
2829
}
2830
2831
/**
2832
 * Handles the "ignored boards" section of the profile (if enabled)
2833
 *
2834
 * @param int $memID The ID of the member
2835
 */
2836
function ignoreboards($memID)
2837
{
2838
	global $context, $modSettings, $smcFunc, $cur_profile, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2839
2840
	// Have the admins enabled this option?
2841
	if (empty($modSettings['allow_ignore_boards']))
2842
		fatal_lang_error('ignoreboards_disallowed', 'user');
2843
2844
	// Find all the boards this user is allowed to see.
2845
	$request = $smcFunc['db_query']('order_by_board_order', '
2846
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
2847
			'. (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
2848
		FROM {db_prefix}boards AS b
2849
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2850
		WHERE {query_see_board}
2851
			AND redirect = {string:empty_string}',
2852
		array(
2853
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
2854
			'empty_string' => '',
2855
		)
2856
	);
2857
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
2858
	$context['categories'] = array();
2859 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2860
	{
2861
		// This category hasn't been set up yet..
2862
		if (!isset($context['categories'][$row['id_cat']]))
2863
			$context['categories'][$row['id_cat']] = array(
2864
				'id' => $row['id_cat'],
2865
				'name' => $row['cat_name'],
2866
				'boards' => array()
2867
			);
2868
2869
		// Set this board up, and let the template know when it's a child.  (indent them..)
2870
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
2871
			'id' => $row['id_board'],
2872
			'name' => $row['name'],
2873
			'child_level' => $row['child_level'],
2874
			'selected' => $row['is_ignored'],
2875
		);
2876
	}
2877
	$smcFunc['db_free_result']($request);
2878
2879
	require_once($sourcedir . '/Subs-Boards.php');
2880
	sortCategories($context['categories']);
2881
2882
	// Now, let's sort the list of categories into the boards for templates that like that.
2883
	$temp_boards = array();
2884 View Code Duplication
	foreach ($context['categories'] as $category)
2885
	{
2886
		// Include a list of boards per category for easy toggling.
2887
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
2888
2889
		$temp_boards[] = array(
2890
			'name' => $category['name'],
2891
			'child_ids' => array_keys($category['boards'])
2892
		);
2893
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
2894
	}
2895
2896
	$max_boards = ceil(count($temp_boards) / 2);
2897
	if ($max_boards == 1)
2898
		$max_boards = 2;
2899
2900
	// Now, alternate them so they can be shown left and right ;).
2901
	$context['board_columns'] = array();
2902 View Code Duplication
	for ($i = 0; $i < $max_boards; $i++)
2903
	{
2904
		$context['board_columns'][] = $temp_boards[$i];
2905
		if (isset($temp_boards[$i + $max_boards]))
2906
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
2907
		else
2908
			$context['board_columns'][] = array();
2909
	}
2910
2911
	loadThemeOptions($memID);
2912
}
2913
2914
/**
2915
 * Load all the languages for the profile
2916
 * .
2917
 * @return bool Whether or not the forum has multiple languages installed
2918
 */
2919
function profileLoadLanguages()
2920
{
2921
	global $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2922
2923
	$context['profile_languages'] = array();
2924
2925
	// Get our languages!
2926
	getLanguages();
2927
2928
	// Setup our languages.
2929
	foreach ($context['languages'] as $lang)
2930
	{
2931
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
2932
	}
2933
	ksort($context['profile_languages']);
2934
2935
	// Return whether we should proceed with this.
2936
	return count($context['profile_languages']) > 1 ? true : false;
2937
}
2938
2939
/**
2940
 * Handles the "manage groups" section of the profile
2941
 *
2942
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2943
 */
2944
function profileLoadGroups()
2945
{
2946
	global $cur_profile, $txt, $context, $smcFunc, $user_settings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2947
2948
	$context['member_groups'] = array(
2949
		0 => array(
2950
			'id' => 0,
2951
			'name' => $txt['no_primary_membergroup'],
2952
			'is_primary' => $cur_profile['id_group'] == 0,
2953
			'can_be_additional' => false,
2954
			'can_be_primary' => true,
2955
		)
2956
	);
2957
	$curGroups = explode(',', $cur_profile['additional_groups']);
2958
2959
	// Load membergroups, but only those groups the user can assign.
2960
	$request = $smcFunc['db_query']('', '
2961
		SELECT group_name, id_group, hidden
2962
		FROM {db_prefix}membergroups
2963
		WHERE id_group != {int:moderator_group}
2964
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
2965
			AND group_type != {int:is_protected}') . '
2966
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
2967
		array(
2968
			'moderator_group' => 3,
2969
			'min_posts' => -1,
2970
			'is_protected' => 1,
2971
			'newbie_group' => 4,
2972
		)
2973
	);
2974
	while ($row = $smcFunc['db_fetch_assoc']($request))
2975
	{
2976
		// We should skip the administrator group if they don't have the admin_forum permission!
2977
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
2978
			continue;
2979
2980
		$context['member_groups'][$row['id_group']] = array(
2981
			'id' => $row['id_group'],
2982
			'name' => $row['group_name'],
2983
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
2984
			'is_additional' => in_array($row['id_group'], $curGroups),
2985
			'can_be_additional' => true,
2986
			'can_be_primary' => $row['hidden'] != 2,
2987
		);
2988
	}
2989
	$smcFunc['db_free_result']($request);
2990
2991
	$context['member']['group_id'] = $user_settings['id_group'];
2992
2993
	return true;
2994
}
2995
2996
/**
2997
 * Load key signature context data.
2998
 *
2999
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3000
 */
3001
function profileLoadSignatureData()
3002
{
3003
	global $modSettings, $context, $txt, $cur_profile, $memberContext;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3004
3005
	// Signature limits.
3006
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3007
	$sig_limits = explode(',', $sig_limits);
3008
3009
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
3010
	$context['signature_limits'] = array(
3011
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
3012
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
3013
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
3014
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
3015
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
3016
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
3017
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
3018
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
3019
	);
3020
	// Kept this line in for backwards compatibility!
3021
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
3022
	// Warning message for signature image limits?
3023
	$context['signature_warning'] = '';
3024
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
3025
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
3026
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
3027
		$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']);
3028
3029
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_charset'] == 'UTF-8' || function_exists('iconv'))));
3030
3031
	if (empty($context['do_preview']))
3032
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
3033
	else
3034
	{
3035
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
3036
		$validation = profileValidateSignature($signature);
3037
		if (empty($context['post_errors']))
3038
		{
3039
			loadLanguage('Errors');
3040
			$context['post_errors'] = array();
3041
		}
3042
		$context['post_errors'][] = 'signature_not_yet_saved';
3043
		if ($validation !== true && $validation !== false)
3044
			$context['post_errors'][] = $validation;
3045
3046
		censorText($context['member']['signature']);
3047
		$context['member']['current_signature'] = $context['member']['signature'];
3048
		censorText($signature);
3049
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']]);
3050
		$context['member']['signature'] = $_POST['signature'];
3051
	}
3052
3053
	// Load the spell checker?
3054 View Code Duplication
	if ($context['show_spellchecking'])
3055
		loadJavaScriptFile('spellcheck.js', array('defer' => false, 'minimize' => true), 'smf_spellcheck');
3056
3057
	return true;
3058
}
3059
3060
/**
3061
 * Load avatar context data.
3062
 *
3063
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3064
 */
3065
function profileLoadAvatarData()
3066
{
3067
	global $context, $cur_profile, $modSettings, $scripturl;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3068
3069
	$context['avatar_url'] = $modSettings['avatar_url'];
3070
3071
	// Default context.
3072
	$context['member']['avatar'] += array(
3073
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
3074
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
3075
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3076
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3077
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
3078
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']) || !empty($modSettings['gravatarOverride']),
3079
	);
3080
3081
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
3082
	{
3083
		$context['member']['avatar'] += array(
3084
			'choice' => 'gravatar',
3085
			'server_pic' => 'blank.png',
3086
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || !empty($modSettings['gravatarOverride']) ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
3087
		);
3088
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
3089
	}
3090
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
3091
	{
3092
		$context['member']['avatar'] += array(
3093
			'choice' => 'upload',
3094
			'server_pic' => 'blank.png',
3095
			'external' => 'http://'
3096
		);
3097
		$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'];
3098
	}
3099
	// Use "avatar_original" here so we show what the user entered even if the image proxy is enabled
3100
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
3101
		$context['member']['avatar'] += array(
3102
			'choice' => 'external',
3103
			'server_pic' => 'blank.png',
3104
			'external' => $cur_profile['avatar_original']
3105
		);
3106
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
3107
		$context['member']['avatar'] += array(
3108
			'choice' => 'server_stored',
3109
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3110
			'external' => 'http://'
3111
		);
3112
	else
3113
		$context['member']['avatar'] += array(
3114
			'choice' => 'none',
3115
			'server_pic' => 'blank.png',
3116
			'external' => 'http://'
3117
		);
3118
3119
	// Get a list of all the avatars.
3120
	if ($context['member']['avatar']['allow_server_stored'])
3121
	{
3122
		$context['avatar_list'] = array();
3123
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3124
	}
3125
	else
3126
		$context['avatars'] = array();
3127
3128
	// Second level selected avatar...
3129
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3130
	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']);
3131
}
3132
3133
/**
3134
 * Save a members group.
3135
 *
3136
 * @param int &$value The ID of the (new) primary group
3137
 * @return true Always returns true
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3138
 */
3139
function profileSaveGroups(&$value)
3140
{
3141
	global $profile_vars, $old_profile, $context, $smcFunc, $cur_profile;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3142
3143
	// Do we need to protect some groups?
3144 View Code Duplication
	if (!allowedTo('admin_forum'))
3145
	{
3146
		$request = $smcFunc['db_query']('', '
3147
			SELECT id_group
3148
			FROM {db_prefix}membergroups
3149
			WHERE group_type = {int:is_protected}',
3150
			array(
3151
				'is_protected' => 1,
3152
			)
3153
		);
3154
		$protected_groups = array(1);
3155
		while ($row = $smcFunc['db_fetch_assoc']($request))
3156
			$protected_groups[] = $row['id_group'];
3157
		$smcFunc['db_free_result']($request);
3158
3159
		$protected_groups = array_unique($protected_groups);
3160
	}
3161
3162
	// The account page allows the change of your id_group - but not to a protected group!
3163
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3164
		$value = (int) $value;
3165
	// ... otherwise it's the old group sir.
3166
	else
3167
		$value = $old_profile['id_group'];
3168
3169
	// Find the additional membergroups (if any)
3170
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3171
	{
3172
		$additional_groups = array();
3173
		foreach ($_POST['additional_groups'] as $group_id)
3174
		{
3175
			$group_id = (int) $group_id;
3176
			if (!empty($group_id) && (empty($protected_groups) || !in_array($group_id, $protected_groups)))
0 ignored issues
show
Bug introduced by
The variable $protected_groups does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3177
				$additional_groups[] = $group_id;
3178
		}
3179
3180
		// Put the protected groups back in there if you don't have permission to take them away.
3181
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3182
		foreach ($old_additional_groups as $group_id)
3183
		{
3184
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3185
				$additional_groups[] = $group_id;
3186
		}
3187
3188
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3189
		{
3190
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3191
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3192
		}
3193
	}
3194
3195
	// Too often, people remove delete their own account, or something.
3196
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3197
	{
3198
		$stillAdmin = $value == 1 || (isset($additional_groups) && in_array(1, $additional_groups));
0 ignored issues
show
Bug introduced by
The variable $additional_groups does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3199
3200
		// If they would no longer be an admin, look for any other...
3201
		if (!$stillAdmin)
3202
		{
3203
			$request = $smcFunc['db_query']('', '
3204
				SELECT id_member
3205
				FROM {db_prefix}members
3206
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3207
					AND id_member != {int:selected_member}
3208
				LIMIT 1',
3209
				array(
3210
					'admin_group' => 1,
3211
					'selected_member' => $context['id_member'],
3212
				)
3213
			);
3214
			list ($another) = $smcFunc['db_fetch_row']($request);
3215
			$smcFunc['db_free_result']($request);
3216
3217
			if (empty($another))
3218
				fatal_lang_error('at_least_one_admin', 'critical');
3219
		}
3220
	}
3221
3222
	// If we are changing group status, update permission cache as necessary.
3223
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3224
	{
3225 View Code Duplication
		if ($context['user']['is_owner'])
3226
			$_SESSION['mc']['time'] = 0;
3227
		else
3228
			updateSettings(array('settings_updated' => time()));
3229
	}
3230
3231
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3232
	call_integration_hook('integrate_profile_profileSaveGroups', array($value, $additional_groups));
3233
3234
	return true;
3235
}
3236
3237
/**
3238
 * The avatar is incredibly complicated, what with the options... and what not.
3239
 * @todo argh, the avatar here. Take this out of here!
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
3240
 *
3241
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3242
 * @return bool|string False if success (or if memID is empty and password authentication failed), otherwise a string indicating what error occurred
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
3243
 */
3244
function profileSaveAvatarData(&$value)
3245
{
3246
	global $modSettings, $sourcedir, $smcFunc, $profile_vars, $cur_profile, $context;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3247
3248
	$memID = $context['id_member'];
3249
	if (empty($memID) && !empty($context['password_auth_failed']))
3250
		return false;
3251
3252
	require_once($sourcedir . '/ManageAttachments.php');
3253
3254
	// We're going to put this on a nice custom dir.
3255
	$uploadDir = $modSettings['custom_avatar_dir'];
3256
	$id_folder = 1;
3257
3258
	$downloadedExternalAvatar = false;
3259
	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']))
3260
	{
3261
		if (!is_writable($uploadDir))
3262
			fatal_lang_error('attachments_no_write', 'critical');
3263
3264
		$url = parse_url($_POST['userpicpersonal']);
3265
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3266
3267
		$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3268
		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 string|false against false; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
3269
		{
3270
			fwrite($tmpAvatar, $contents);
3271
			fclose($tmpAvatar);
3272
3273
			$downloadedExternalAvatar = true;
3274
			$_FILES['attachment']['tmp_name'] = $new_filename;
3275
		}
3276
	}
3277
3278
	// Removes whatever attachment there was before updating
3279
	if ($value == 'none')
3280
	{
3281
		$profile_vars['avatar'] = '';
3282
3283
		// Reset the attach ID.
3284
		$cur_profile['id_attach'] = 0;
3285
		$cur_profile['attachment_type'] = 0;
3286
		$cur_profile['filename'] = '';
3287
3288
		removeAttachments(array('id_member' => $memID));
3289
	}
3290
3291
	// An avatar from the server-stored galleries.
3292
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3293
	{
3294
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3295
		$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']) : '';
3296
3297
		// Clear current profile...
3298
		$cur_profile['id_attach'] = 0;
3299
		$cur_profile['attachment_type'] = 0;
3300
		$cur_profile['filename'] = '';
3301
3302
		// Get rid of their old avatar. (if uploaded.)
3303
		removeAttachments(array('id_member' => $memID));
3304
	}
3305
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3306
	{
3307
		// 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.
3308
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3309
			$profile_vars['avatar'] = 'gravatar://';
3310
		else
3311
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3312
3313
		// Get rid of their old avatar. (if uploaded.)
3314
		removeAttachments(array('id_member' => $memID));
3315
	}
3316
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3317
	{
3318
		// We need these clean...
3319
		$cur_profile['id_attach'] = 0;
3320
		$cur_profile['attachment_type'] = 0;
3321
		$cur_profile['filename'] = '';
3322
3323
		// Remove any attached avatar...
3324
		removeAttachments(array('id_member' => $memID));
3325
3326
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3327
3328
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3329
			$profile_vars['avatar'] = '';
3330
		// Trying to make us do something we'll regret?
3331
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3332
			return 'bad_avatar_invalid_url';
3333
		// Should we check dimensions?
3334
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3335
		{
3336
			// Now let's validate the avatar.
3337
			$sizes = url_image_size($profile_vars['avatar']);
3338
3339
			if (is_array($sizes) && (($sizes[0] > $modSettings['avatar_max_width_external'] && !empty($modSettings['avatar_max_width_external'])) || ($sizes[1] > $modSettings['avatar_max_height_external'] && !empty($modSettings['avatar_max_height_external']))))
3340
			{
3341
				// Houston, we have a problem. The avatar is too large!!
3342
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3343
					return 'bad_avatar_too_large';
3344
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3345
				{
3346
					// @todo remove this if appropriate
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
3347
					require_once($sourcedir . '/Subs-Graphics.php');
3348
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3349
					{
3350
						$profile_vars['avatar'] = '';
3351
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3352
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3353
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3354
					}
3355
					else
3356
						return 'bad_avatar';
3357
				}
3358
			}
3359
		}
3360
	}
3361
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3362
	{
3363
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3364
		{
3365
			// Get the dimensions of the image.
3366
			if (!$downloadedExternalAvatar)
3367
			{
3368
				if (!is_writable($uploadDir))
3369
					fatal_lang_error('attachments_no_write', 'critical');
3370
3371
				$new_filename = $uploadDir . '/' . getAttachmentFilename('avatar_tmp_' . $memID, false, null, true);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3372
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3373
					fatal_lang_error('attach_timeout', 'critical');
3374
3375
				$_FILES['attachment']['tmp_name'] = $new_filename;
3376
			}
3377
3378
			$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3379
3380
			// No size, then it's probably not a valid pic.
3381
			if ($sizes === false)
3382
			{
3383
				@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3384
				return 'bad_avatar';
3385
			}
3386
			// Check whether the image is too large.
3387
			elseif ((!empty($modSettings['avatar_max_width_upload']) && $sizes[0] > $modSettings['avatar_max_width_upload']) || (!empty($modSettings['avatar_max_height_upload']) && $sizes[1] > $modSettings['avatar_max_height_upload']))
3388
			{
3389
				if (!empty($modSettings['avatar_resize_upload']))
3390
				{
3391
					// Attempt to chmod it.
3392
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3393
3394
					// @todo remove this require when appropriate
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
3395
					require_once($sourcedir . '/Subs-Graphics.php');
3396
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3397
					{
3398
						@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3399
						return 'bad_avatar';
3400
					}
3401
3402
					// Reset attachment avatar data.
3403
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3404
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3405
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3406
				}
3407
3408
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3409
				else
3410
				{
3411
					@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3412
					return 'bad_avatar_too_large';
3413
				}
3414
			}
3415
3416
			// So far, so good, checks lies ahead!
3417
			elseif (is_array($sizes))
3418
			{
3419
				// Now try to find an infection.
3420
				require_once($sourcedir . '/Subs-Graphics.php');
3421
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3422
				{
3423
					// It's bad. Try to re-encode the contents?
3424
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3425
					{
3426
						@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3427
						return 'bad_avatar_fail_reencode';
3428
					}
3429
					// We were successful. However, at what price?
3430
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3431
					// Hard to believe this would happen, but can you bet?
3432
					if ($sizes === false)
3433
					{
3434
						@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3435
						return 'bad_avatar';
3436
					}
3437
				}
3438
3439
				$extensions = array(
3440
					'1' => 'gif',
3441
					'2' => 'jpg',
3442
					'3' => 'png',
3443
					'6' => 'bmp'
3444
				);
3445
3446
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3447
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3448
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3449
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3450
				$file_hash = '';
3451
3452
				// Remove previous attachments this member might have had.
3453
				removeAttachments(array('id_member' => $memID));
3454
3455
				$cur_profile['id_attach'] = $smcFunc['db_insert']('',
3456
					'{db_prefix}attachments',
3457
					array(
3458
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3459
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3460
					),
3461
					array(
3462
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3463
						(int) $width, (int) $height, $mime_type, $id_folder,
3464
					),
3465
					array('id_attach'),
3466
					1
3467
				);
3468
3469
				$cur_profile['filename'] = $destName;
3470
				$cur_profile['attachment_type'] = 1;
3471
3472
				$destinationPath = $uploadDir . '/' . (empty($file_hash) ? $destName : $cur_profile['id_attach'] . '_' . $file_hash . '.dat');
3473
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3474
				{
3475
					// I guess a man can try.
3476
					removeAttachments(array('id_member' => $memID));
3477
					fatal_lang_error('attach_timeout', 'critical');
3478
				}
3479
3480
				// Attempt to chmod it.
3481
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3482
			}
3483
			$profile_vars['avatar'] = '';
3484
3485
			// Delete any temporary file.
3486
			if (file_exists($_FILES['attachment']['tmp_name']))
3487
				@unlink($_FILES['attachment']['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3488
		}
3489
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3490
		else
3491
			$profile_vars['avatar'] = '';
3492
	}
3493
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3494
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3495
	else
3496
		$profile_vars['avatar'] = '';
3497
3498
	// Setup the profile variables so it shows things right on display!
3499
	$cur_profile['avatar'] = $profile_vars['avatar'];
3500
3501
	return false;
3502
}
3503
3504
/**
3505
 * Validate the signature
3506
 *
3507
 * @param string &$value The new signature
3508
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3509
 */
3510
function profileValidateSignature(&$value)
3511
{
3512
	global $sourcedir, $modSettings, $smcFunc, $txt;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3513
3514
	require_once($sourcedir . '/Subs-Post.php');
3515
3516
	// Admins can do whatever they hell they want!
3517
	if (!allowedTo('admin_forum'))
3518
	{
3519
		// Load all the signature limits.
3520
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3521
		$sig_limits = explode(',', $sig_limits);
3522
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3523
3524
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3525
3526
		// Too many lines?
3527
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3528
		{
3529
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3530
			return 'signature_max_lines';
3531
		}
3532
3533
		// Too many images?!
3534 View Code Duplication
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
3535
		{
3536
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3537
			return 'signature_max_image_count';
3538
		}
3539
3540
		// What about too many smileys!
3541
		$smiley_parsed = $unparsed_signature;
3542
		parsesmileys($smiley_parsed);
3543
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3544
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3545
			return 'signature_allow_smileys';
3546
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3547
		{
3548
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3549
			return 'signature_max_smileys';
3550
		}
3551
3552
		// Maybe we are abusing font sizes?
3553
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3554
		{
3555
			foreach ($matches[1] as $ind => $size)
3556
			{
3557
				$limit_broke = 0;
3558
				// Attempt to allow all sizes of abuse, so to speak.
3559 View Code Duplication
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
3560
					$limit_broke = $sig_limits[7] . 'px';
3561
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3562
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3563
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3564
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3565
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3566
					$limit_broke = 'large';
3567
3568
				if ($limit_broke)
3569
				{
3570
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3571
					return 'signature_max_font_size';
3572
				}
3573
			}
3574
		}
3575
3576
		// The difficult one - image sizes! Don't error on this - just fix it.
3577
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3578
		{
3579
			// Get all BBC tags...
3580
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3581
			// ... and all HTML ones.
3582
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?' . '>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3583
			// And stick the HTML in the BBC.
3584 View Code Duplication
			if (!empty($matches2))
3585
			{
3586
				foreach ($matches2[0] as $ind => $dummy)
3587
				{
3588
					$matches[0][] = $matches2[0][$ind];
3589
					$matches[1][] = '';
3590
					$matches[2][] = '';
3591
					$matches[3][] = '';
3592
					$matches[4][] = '';
3593
					$matches[5][] = '';
3594
					$matches[6][] = '';
3595
					$matches[7][] = $matches2[1][$ind];
3596
				}
3597
			}
3598
3599
			$replaces = array();
3600
			// Try to find all the images!
3601
			if (!empty($matches))
3602
			{
3603
				foreach ($matches[0] as $key => $image)
3604
				{
3605
					$width = -1; $height = -1;
0 ignored issues
show
Coding Style introduced by
It is generally recommended to place each PHP statement on a line by itself.

Let’s take a look at an example:

// Bad
$a = 5; $b = 6; $c = 7;

// Good
$a = 5;
$b = 6;
$c = 7;
Loading history...
3606
3607
					// Does it have predefined restraints? Width first.
3608 View Code Duplication
					if ($matches[6][$key])
3609
						$matches[2][$key] = $matches[6][$key];
3610 View Code Duplication
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
3611
					{
3612
						$width = $sig_limits[5];
3613
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3614
					}
3615
					elseif ($matches[2][$key])
3616
						$width = $matches[2][$key];
3617
					// ... and height.
3618 View Code Duplication
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
3619
					{
3620
						$height = $sig_limits[6];
3621
						if ($width != -1)
3622
							$width = $width * ($height / $matches[4][$key]);
3623
					}
3624
					elseif ($matches[4][$key])
3625
						$height = $matches[4][$key];
3626
3627
					// If the dimensions are still not fixed - we need to check the actual image.
3628 View Code Duplication
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
3629
					{
3630
						$sizes = url_image_size($matches[7][$key]);
3631
						if (is_array($sizes))
3632
						{
3633
							// Too wide?
3634
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3635
							{
3636
								$width = $sig_limits[5];
3637
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3638
							}
3639
							// Too high?
3640
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3641
							{
3642
								$height = $sig_limits[6];
3643
								if ($width == -1)
3644
									$width = $sizes[0];
3645
								$width = $width * ($height / $sizes[1]);
3646
							}
3647
							elseif ($width != -1)
3648
								$height = $sizes[1];
3649
						}
3650
					}
3651
3652
					// Did we come up with some changes? If so remake the string.
3653 View Code Duplication
					if ($width != -1 || $height != -1)
3654
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3655
				}
3656
				if (!empty($replaces))
3657
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3658
			}
3659
		}
3660
3661
		// Any disabled BBC?
3662
		$disabledSigBBC = implode('|', $disabledTags);
3663
		if (!empty($disabledSigBBC))
3664
		{
3665
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3666
			{
3667
				$disabledTags = array_unique($disabledTags);
3668
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3669
				return 'signature_disabled_bbc';
3670
			}
3671
		}
3672
	}
3673
3674
	preparsecode($value);
3675
3676
	// Too long?
3677
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3678
	{
3679
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3680
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3681
		return 'signature_max_length';
3682
	}
3683
3684
	return true;
3685
}
3686
3687
/**
3688
 * Validate an email address.
3689
 *
3690
 * @param string $email The email address to validate
3691
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3692
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3693
 */
3694
function profileValidateEmail($email, $memID = 0)
3695
{
3696
	global $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3697
3698
	$email = strtr($email, array('&#039;' => '\''));
3699
3700
	// Check the name and email for validity.
3701
	if (trim($email) == '')
3702
		return 'no_email';
3703
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3704
		return 'bad_email';
3705
3706
	// Email addresses should be and stay unique.
3707
	$request = $smcFunc['db_query']('', '
3708
		SELECT id_member
3709
		FROM {db_prefix}members
3710
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3711
			email_address = {string:email_address}
3712
		LIMIT 1',
3713
		array(
3714
			'selected_member' => $memID,
3715
			'email_address' => $email,
3716
		)
3717
	);
3718
3719
	if ($smcFunc['db_num_rows']($request) > 0)
3720
		return 'email_taken';
3721
	$smcFunc['db_free_result']($request);
3722
3723
	return true;
3724
}
3725
3726
/**
3727
 * Reload a user's settings.
3728
 */
3729
function profileReloadUser()
3730
{
3731
	global $modSettings, $context, $cur_profile;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3732
3733
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3734
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3735
3736
	loadUserSettings();
3737
	writeLog();
3738
}
3739
3740
/**
3741
 * Send the user a new activation email if they need to reactivate!
3742
 */
3743
function profileSendActivation()
3744
{
3745
	global $sourcedir, $profile_vars, $context, $scripturl, $smcFunc, $cookiename, $cur_profile, $language, $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3746
3747
	require_once($sourcedir . '/Subs-Post.php');
3748
3749
	// Shouldn't happen but just in case.
3750
	if (empty($profile_vars['email_address']))
3751
		return;
3752
3753
	$replacements = array(
3754
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3755
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3756
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3757
	);
3758
3759
	// Send off the email.
3760
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3761
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3762
3763
	// Log the user out.
3764
	$smcFunc['db_query']('', '
3765
		DELETE FROM {db_prefix}log_online
3766
		WHERE id_member = {int:selected_member}',
3767
		array(
3768
			'selected_member' => $context['id_member'],
3769
		)
3770
	);
3771
	$_SESSION['log_time'] = 0;
3772
	$_SESSION['login_' . $cookiename] = $smcFunc['json_encode'](array(0, '', 0));
3773
3774
	if (isset($_COOKIE[$cookiename]))
3775
		$_COOKIE[$cookiename] = '';
3776
3777
	loadUserSettings();
3778
3779
	$context['user']['is_logged'] = false;
3780
	$context['user']['is_guest'] = true;
3781
3782
	redirectexit('action=sendactivation');
3783
}
3784
3785
/**
3786
 * Function to allow the user to choose group membership etc...
3787
 *
3788
 * @param int $memID The ID of the member
3789
 */
3790
function groupMembership($memID)
3791
{
3792
	global $txt, $user_profile, $context, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3793
3794
	$curMember = $user_profile[$memID];
3795
	$context['primary_group'] = $curMember['id_group'];
3796
3797
	// Can they manage groups?
3798
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3799
	$context['can_manage_protected'] = allowedTo('admin_forum');
3800
	$context['can_edit_primary'] = $context['can_manage_protected'];
3801
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
3802
3803
	// Get all the groups this user is a member of.
3804
	$groups = explode(',', $curMember['additional_groups']);
3805
	$groups[] = $curMember['id_group'];
3806
3807
	// Ensure the query doesn't croak!
3808
	if (empty($groups))
3809
		$groups = array(0);
3810
	// Just to be sure...
3811
	foreach ($groups as $k => $v)
3812
		$groups[$k] = (int) $v;
3813
3814
	// Get all the membergroups they can join.
3815
	$request = $smcFunc['db_query']('', '
3816
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
3817
			COALESCE(lgr.id_member, 0) AS pending
3818
		FROM {db_prefix}membergroups AS mg
3819
			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})
3820
		WHERE (mg.id_group IN ({array_int:group_list})
3821
			OR mg.group_type > {int:nonjoin_group_id})
3822
			AND mg.min_posts = {int:min_posts}
3823
			AND mg.id_group != {int:moderator_group}
3824
		ORDER BY group_name',
3825
		array(
3826
			'group_list' => $groups,
3827
			'selected_member' => $memID,
3828
			'status_open' => 0,
3829
			'nonjoin_group_id' => 1,
3830
			'min_posts' => -1,
3831
			'moderator_group' => 3,
3832
		)
3833
	);
3834
	// This beast will be our group holder.
3835
	$context['groups'] = array(
3836
		'member' => array(),
3837
		'available' => array()
3838
	);
3839
	while ($row = $smcFunc['db_fetch_assoc']($request))
3840
	{
3841
		// Can they edit their primary group?
3842
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
3843
			$context['can_edit_primary'] = true;
3844
3845
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
3846
		if (((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0)) && $row['id_group'] != $context['primary_group'])
3847
			continue;
3848
3849
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
3850
			'id' => $row['id_group'],
3851
			'name' => $row['group_name'],
3852
			'desc' => $row['description'],
3853
			'color' => $row['online_color'],
3854
			'type' => $row['group_type'],
3855
			'pending' => $row['pending'],
3856
			'is_primary' => $row['id_group'] == $context['primary_group'],
3857
			'can_be_primary' => $row['hidden'] != 2,
3858
			// Anything more than this needs to be done through account settings for security.
3859
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
3860
		);
3861
	}
3862
	$smcFunc['db_free_result']($request);
3863
3864
	// Add registered members on the end.
3865
	$context['groups']['member'][0] = array(
3866
		'id' => 0,
3867
		'name' => $txt['regular_members'],
3868
		'desc' => $txt['regular_members_desc'],
3869
		'type' => 0,
3870
		'is_primary' => $context['primary_group'] == 0 ? true : false,
3871
		'can_be_primary' => true,
3872
		'can_leave' => 0,
3873
	);
3874
3875
	// No changing primary one unless you have enough groups!
3876
	if (count($context['groups']['member']) < 2)
3877
		$context['can_edit_primary'] = false;
3878
3879
	// In the special case that someone is requesting membership of a group, setup some special context vars.
3880
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
3881
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
3882
}
3883
3884
/**
3885
 * This function actually makes all the group changes
3886
 *
3887
 * @param array $profile_vars The profile variables
3888
 * @param array $post_errors Any errors that have occurred
3889
 * @param int $memID The ID of the member
3890
 * @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
3891
 */
3892
function groupMembership2($profile_vars, $post_errors, $memID)
0 ignored issues
show
Unused Code introduced by
The parameter $profile_vars is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3893
{
3894
	global $user_info, $context, $user_profile, $modSettings, $smcFunc;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
3895
3896
	// Let's be extra cautious...
3897
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
3898
		isAllowedTo('manage_membergroups');
3899
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
3900
		fatal_lang_error('no_access', false);
3901
3902
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
3903
3904
	$old_profile = &$user_profile[$memID];
3905
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3906
	$context['can_manage_protected'] = allowedTo('admin_forum');
3907
3908
	// By default the new primary is the old one.
3909
	$newPrimary = $old_profile['id_group'];
3910
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
3911
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
3912
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
3913
3914
	// One way or another, we have a target group in mind...
3915
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
3916
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
3917
3918
	// Sanity check!!
3919
	if ($group_id == 1)
3920
		isAllowedTo('admin_forum');
3921
	// Protected groups too!
3922
	else
3923
	{
3924
		$request = $smcFunc['db_query']('', '
3925
			SELECT group_type
3926
			FROM {db_prefix}membergroups
3927
			WHERE id_group = {int:current_group}
3928
			LIMIT {int:limit}',
3929
			array(
3930
				'current_group' => $group_id,
3931
				'limit' => 1,
3932
			)
3933
		);
3934
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
3935
		$smcFunc['db_free_result']($request);
3936
3937
		if ($is_protected == 1)
3938
			isAllowedTo('admin_forum');
3939
	}
3940
3941
	// What ever we are doing, we need to determine if changing primary is possible!
3942
	$request = $smcFunc['db_query']('', '
3943
		SELECT id_group, group_type, hidden, group_name
3944
		FROM {db_prefix}membergroups
3945
		WHERE id_group IN ({int:group_list}, {int:current_group})',
3946
		array(
3947
			'group_list' => $group_id,
3948
			'current_group' => $old_profile['id_group'],
3949
		)
3950
	);
3951
	while ($row = $smcFunc['db_fetch_assoc']($request))
3952
	{
3953
		// Is this the new group?
3954
		if ($row['id_group'] == $group_id)
3955
		{
3956
			$foundTarget = true;
3957
			$group_name = $row['group_name'];
3958
3959
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
3960
			if ($changeType == 'request' && $row['group_type'] != 2)
3961
				fatal_lang_error('no_access', false);
3962
			// What about leaving a requestable group we are not a member of?
3963
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
3964
				fatal_lang_error('no_access', false);
3965
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
3966
				fatal_lang_error('no_access', false);
3967
3968
			// We can't change the primary group if this is hidden!
3969
			if ($row['hidden'] == 2)
3970
				$canChangePrimary = false;
3971
		}
3972
3973
		// If this is their old primary, can we change it?
3974 View Code Duplication
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
3975
			$canChangePrimary = 1;
3976
3977
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
3978
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
3979
			$canChangePrimary = false;
3980
3981
		// If this is the one we are acting on, can we even act?
3982 View Code Duplication
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
3983
			$canChangePrimary = false;
3984
	}
3985
	$smcFunc['db_free_result']($request);
3986
3987
	// Didn't find the target?
3988
	if (!$foundTarget)
3989
		fatal_lang_error('no_access', false);
3990
3991
	// Final security check, don't allow users to promote themselves to admin.
3992
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
3993
	{
3994
		$request = $smcFunc['db_query']('', '
3995
			SELECT COUNT(permission)
3996
			FROM {db_prefix}permissions
3997
			WHERE id_group = {int:selected_group}
3998
				AND permission = {string:admin_forum}
3999
				AND add_deny = {int:not_denied}',
4000
			array(
4001
				'selected_group' => $group_id,
4002
				'not_denied' => 1,
4003
				'admin_forum' => 'admin_forum',
4004
			)
4005
		);
4006
		list ($disallow) = $smcFunc['db_fetch_row']($request);
4007
		$smcFunc['db_free_result']($request);
4008
4009
		if ($disallow)
4010
			isAllowedTo('admin_forum');
4011
	}
4012
4013
	// If we're requesting, add the note then return.
4014
	if ($changeType == 'request')
4015
	{
4016
		$request = $smcFunc['db_query']('', '
4017
			SELECT id_member
4018
			FROM {db_prefix}log_group_requests
4019
			WHERE id_member = {int:selected_member}
4020
				AND id_group = {int:selected_group}
4021
				AND status = {int:status_open}',
4022
			array(
4023
				'selected_member' => $memID,
4024
				'selected_group' => $group_id,
4025
				'status_open' => 0,
4026
			)
4027
		);
4028
		if ($smcFunc['db_num_rows']($request) != 0)
4029
			fatal_lang_error('profile_error_already_requested_group');
4030
		$smcFunc['db_free_result']($request);
4031
4032
		// Log the request.
4033
		$smcFunc['db_insert']('',
4034
			'{db_prefix}log_group_requests',
4035
			array(
4036
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
4037
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
4038
			),
4039
			array(
4040
				$memID, $group_id, time(), $_POST['reason'],
4041
				0, 0, '', 0, '',
4042
			),
4043
			array('id_request')
4044
		);
4045
4046
		// Set up some data for our background task...
4047
		$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
Bug introduced by
The variable $group_name does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
4048
4049
		// Add a background task to handle notifying people of this request
4050
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
4051
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
4052
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
4053
		);
4054
4055
		return $changeType;
4056
	}
4057
	// Otherwise we are leaving/joining a group.
4058
	elseif ($changeType == 'free')
4059
	{
4060
		// Are we leaving?
4061
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
4062
		{
4063
			if ($old_profile['id_group'] == $group_id)
4064
				$newPrimary = 0;
4065
			else
4066
				unset($addGroups[$group_id]);
4067
		}
4068
		// ... if not, must be joining.
4069
		else
4070
		{
4071
			// Can we change the primary, and do we want to?
4072
			if ($canChangePrimary)
0 ignored issues
show
Bug Best Practice introduced by
The expression $canChangePrimary of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
4073
			{
4074
				if ($old_profile['id_group'] != 0)
4075
					$addGroups[$old_profile['id_group']] = -1;
4076
				$newPrimary = $group_id;
4077
			}
4078
			// Otherwise it's an additional group...
4079
			else
4080
				$addGroups[$group_id] = -1;
4081
		}
4082
	}
4083
	// Finally, we must be setting the primary.
4084
	elseif ($canChangePrimary)
0 ignored issues
show
Bug Best Practice introduced by
The expression $canChangePrimary of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
4085
	{
4086
		if ($old_profile['id_group'] != 0)
4087
			$addGroups[$old_profile['id_group']] = -1;
4088
		if (isset($addGroups[$group_id]))
4089
			unset($addGroups[$group_id]);
4090
		$newPrimary = $group_id;
4091
	}
4092
4093
	// Finally, we can make the changes!
4094
	foreach ($addGroups as $id => $dummy)
4095
		if (empty($id))
4096
			unset($addGroups[$id]);
4097
	$addGroups = implode(',', array_flip($addGroups));
4098
4099
	// Ensure that we don't cache permissions if the group is changing.
4100 View Code Duplication
	if ($context['user']['is_owner'])
4101
		$_SESSION['mc']['time'] = 0;
4102
	else
4103
		updateSettings(array('settings_updated' => time()));
4104
4105
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
4106
4107
	return $changeType;
4108
}
4109
4110
/**
4111
 * Provides interface to setup Two Factor Auth in SMF
4112
 *
4113
 * @param int $memID The ID of the member
4114
 */
4115
function tfasetup($memID)
4116
{
4117
	global $user_info, $context, $user_settings, $sourcedir, $modSettings;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
4118
4119
	require_once($sourcedir . '/Class-TOTP.php');
4120
	require_once($sourcedir . '/Subs-Auth.php');
4121
4122
	// If TFA has not been setup, allow them to set it up
4123
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4124
	{
4125
		// Check to ensure we're forcing SSL for authentication
4126 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
0 ignored issues
show
Bug introduced by
The variable $maintenance seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
4127
			fatal_lang_error('login_ssl_required');
4128
4129
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4130
		// we do not want too much AJAX to confuse them.
4131
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4132
		{
4133
			$context['from_ajax'] = true;
4134
			$context['template_layers'] = array();
4135
		}
4136
4137
		// When the code is being sent, verify to make sure the user got it right
4138
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4139
		{
4140
			$code = $_POST['tfa_code'];
4141
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4142
			$totp->setRange(1);
4143
			$valid_password = hash_verify_password($user_settings['member_name'], trim($_POST['passwd']), $user_settings['passwd']);
4144
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4145
4146
			if ($valid_password && $valid_code)
4147
			{
4148
				$backup = substr(sha1(mt_rand()), 0, 16);
4149
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4150
4151
				updateMemberData($memID, array(
4152
					'tfa_secret' => $_SESSION['tfa_secret'],
4153
					'tfa_backup' => $backup_encrypted,
4154
				));
4155
4156
				setTFACookie(3153600, $memID, hash_salt($backup_encrypted, $user_settings['password_salt']));
0 ignored issues
show
Bug introduced by
It seems like $backup_encrypted defined by hash_password($user_sett...member_name'], $backup) on line 4149 can also be of type false or null; however, hash_salt() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4157
4158
				unset($_SESSION['tfa_secret']);
4159
4160
				$context['tfa_backup'] = $backup;
4161
				$context['sub_template'] = 'tfasetup_backup';
4162
4163
				return;
4164
			}
4165
			else
4166
			{
4167
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4168
				$context['tfa_error'] = !$valid_code;
4169
				$context['tfa_pass_error'] = !$valid_password;
4170
				$context['tfa_pass_value'] = $_POST['passwd'];
4171
				$context['tfa_value'] = $_POST['tfa_code'];
4172
			}
4173
		}
4174
		else
4175
		{
4176
			$totp = new \TOTP\Auth();
4177
			$secret = $totp->generateCode();
4178
			$_SESSION['tfa_secret'] = $secret;
4179
			$context['tfa_secret'] = $secret;
4180
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4181
		}
4182
4183
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4184
	}
4185
	elseif (isset($_REQUEST['disable']))
4186
	{
4187
		updateMemberData($memID, array(
4188
			'tfa_secret' => '',
4189
			'tfa_backup' => '',
4190
		));
4191
		redirectexit('action=profile;area=account;u=' . $memID);
4192
	}
4193
	else
4194
		redirectexit('action=profile;area=account;u=' . $memID);
4195
}
4196
4197
?>