Completed
Push — release-2.1 ( 4e0460...a0d750 )
by
unknown
10:08
created

Profile-Modify.php ➔ theme()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 1
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
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']) ? '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 != "'. $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
				return true;
592
			},
593
			'link_with' => 'website',
594
		),
595
	);
596
597
	call_integration_hook('integrate_load_profile_fields', array(&$profile_fields));
598
599
	$disabled_fields = !empty($modSettings['disabled_profile_fields']) ? explode(',', $modSettings['disabled_profile_fields']) : array();
600
	// For each of the above let's take out the bits which don't apply - to save memory and security!
601
	foreach ($profile_fields as $key => $field)
602
	{
603
		// Do we have permission to do this?
604
		if (isset($field['permission']) && !allowedTo(($context['user']['is_owner'] ? array($field['permission'] . '_own', $field['permission'] . '_any') : $field['permission'] . '_any')) && !allowedTo($field['permission']))
605
			unset($profile_fields[$key]);
606
607
		// Is it enabled?
608
		if (isset($field['enabled']) && !$field['enabled'])
609
			unset($profile_fields[$key]);
610
611
		// Is it specifically disabled?
612
		if (in_array($key, $disabled_fields) || (isset($field['link_with']) && in_array($field['link_with'], $disabled_fields)))
613
			unset($profile_fields[$key]);
614
	}
615
}
616
617
/**
618
 * Setup the context for a page load!
619
 *
620
 * @param array $fields The profile fields to display. Each item should correspond to an item in the $profile_fields array generated by loadProfileFields
621
 */
622
function setupProfileContext($fields)
623
{
624
	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...
625
626
	// Some default bits.
627
	$context['profile_prehtml'] = '';
628
	$context['profile_posthtml'] = '';
629
	$context['profile_javascript'] = '';
630
	$context['profile_onsubmit_javascript'] = '';
631
632
	call_integration_hook('integrate_setup_profile_context', array(&$fields));
633
634
	// Make sure we have this!
635
	loadProfileFields(true);
636
637
	// First check for any linked sets.
638
	foreach ($profile_fields as $key => $field)
639
		if (isset($field['link_with']) && in_array($field['link_with'], $fields))
640
			$fields[] = $key;
641
642
	$i = 0;
643
	$last_type = '';
644
	foreach ($fields as $key => $field)
645
	{
646
		if (isset($profile_fields[$field]))
647
		{
648
			// Shortcut.
649
			$cur_field = &$profile_fields[$field];
650
651
			// Does it have a preload and does that preload succeed?
652
			if (isset($cur_field['preload']) && !$cur_field['preload']())
653
				continue;
654
655
			// If this is anything but complex we need to do more cleaning!
656
			if ($cur_field['type'] != 'callback' && $cur_field['type'] != 'hidden')
657
			{
658
				if (!isset($cur_field['label']))
659
					$cur_field['label'] = isset($txt[$field]) ? $txt[$field] : $field;
660
661
				// Everything has a value!
662
				if (!isset($cur_field['value']))
663
					$cur_field['value'] = isset($cur_profile[$field]) ? $cur_profile[$field] : '';
664
665
				// Any input attributes?
666
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
667
			}
668
669
			// Was there an error with this field on posting?
670
			if (isset($context['profile_errors'][$field]))
671
				$cur_field['is_error'] = true;
672
673
			// Any javascript stuff?
674
			if (!empty($cur_field['js_submit']))
675
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
676
			if (!empty($cur_field['js']))
677
				$context['profile_javascript'] .= $cur_field['js'];
678
679
			// Any template stuff?
680
			if (!empty($cur_field['prehtml']))
681
				$context['profile_prehtml'] .= $cur_field['prehtml'];
682
			if (!empty($cur_field['posthtml']))
683
				$context['profile_posthtml'] .= $cur_field['posthtml'];
684
685
			// Finally put it into context?
686
			if ($cur_field['type'] != 'hidden')
687
			{
688
				$last_type = $cur_field['type'];
689
				$context['profile_fields'][$field] = &$profile_fields[$field];
690
			}
691
		}
692
		// Bodge in a line break - without doing two in a row ;)
693
		elseif ($field == 'hr' && $last_type != 'hr' && $last_type != '')
694
		{
695
			$last_type = 'hr';
696
			$context['profile_fields'][$i++]['type'] = 'hr';
697
		}
698
	}
699
700
	// Some spicy JS.
701
	addInlineJavaScript('
702
	var form_handle = document.forms.creator;
703
	createEventListener(form_handle);
704
	'. (!empty($context['require_password']) ? '
705
	form_handle.addEventListener(\'submit\', function(event)
706
	{
707
		if (this.oldpasswrd.value == "")
708
		{
709
			event.preventDefault();
710
			alert('. (JavaScriptEscape($txt['required_security_reasons'])) . ');
711
			return false;
712
		}
713
	}, false);' : ''), true);
714
715
	// Any onsubmit javascript?
716
	if (!empty($context['profile_onsubmit_javascript']))
717
		addInlineJavaScript($context['profile_onsubmit_javascript'], true);
718
719
	// Any totally custom stuff?
720
	if (!empty($context['profile_javascript']))
721
		addInlineJavaScript($context['profile_javascript'], true);
722
723
	// Free up some memory.
724
	unset($profile_fields);
725
}
726
727
/**
728
 * Save the profile changes.
729
 */
730
function saveProfileFields()
731
{
732
	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...
733
734
	// Load them up.
735
	loadProfileFields();
736
737
	// This makes things easier...
738
	$old_profile = $cur_profile;
739
740
	// This allows variables to call activities when they save - by default just to reload their settings
741
	$context['profile_execute_on_save'] = array();
742
	if ($context['user']['is_owner'])
743
		$context['profile_execute_on_save']['reload_user'] = 'profileReloadUser';
744
745
	// Assume we log nothing.
746
	$context['log_changes'] = array();
747
748
	// Cycle through the profile fields working out what to do!
749
	foreach ($profile_fields as $key => $field)
750
	{
751
		if (!isset($_POST[$key]) || !empty($field['is_dummy']) || (isset($_POST['preview_signature']) && $key == 'signature'))
752
			continue;
753
754
		// What gets updated?
755
		$db_key = isset($field['save_key']) ? $field['save_key'] : $key;
756
757
		// Right - we have something that is enabled, we can act upon and has a value posted to it. Does it have a validation function?
758
		if (isset($field['input_validate']))
759
		{
760
			$is_valid = $field['input_validate']($_POST[$key]);
761
			// An error occurred - set it as such!
762
			if ($is_valid !== true)
763
			{
764
				// Is this an actual error?
765
				if ($is_valid !== false)
766
				{
767
					$post_errors[$key] = $is_valid;
768
					$profile_fields[$key]['is_error'] = $is_valid;
769
				}
770
				// Retain the old value.
771
				$cur_profile[$key] = $_POST[$key];
772
				continue;
773
			}
774
		}
775
776
		// Are we doing a cast?
777
		$field['cast_type'] = empty($field['cast_type']) ? $field['type'] : $field['cast_type'];
778
779
		// Finally, clean up certain types.
780
		if ($field['cast_type'] == 'int')
781
			$_POST[$key] = (int) $_POST[$key];
782
		elseif ($field['cast_type'] == 'float')
783
			$_POST[$key] = (float) $_POST[$key];
784
		elseif ($field['cast_type'] == 'check')
785
			$_POST[$key] = !empty($_POST[$key]) ? 1 : 0;
786
787
		// If we got here we're doing OK.
788
		if ($field['type'] != 'hidden' && (!isset($old_profile[$key]) || $_POST[$key] != $old_profile[$key]))
789
		{
790
			// Set the save variable.
791
			$profile_vars[$db_key] = $_POST[$key];
792
			// And update the user profile.
793
			$cur_profile[$key] = $_POST[$key];
794
795
			// Are we logging it?
796
			if (!empty($field['log_change']) && isset($old_profile[$key]))
797
				$context['log_changes'][$key] = array(
798
					'previous' => $old_profile[$key],
799
					'new' => $_POST[$key],
800
				);
801
		}
802
803
		// Logging group changes are a bit different...
804
		if ($key == 'id_group' && $field['log_change'])
805
		{
806
			profileLoadGroups();
807
808
			// Any changes to primary group?
809
			if ($_POST['id_group'] != $old_profile['id_group'])
810
			{
811
				$context['log_changes']['id_group'] = array(
812
					'previous' => !empty($old_profile[$key]) && isset($context['member_groups'][$old_profile[$key]]) ? $context['member_groups'][$old_profile[$key]]['name'] : '',
813
					'new' => !empty($_POST[$key]) && isset($context['member_groups'][$_POST[$key]]) ? $context['member_groups'][$_POST[$key]]['name'] : '',
814
				);
815
			}
816
817
			// Prepare additional groups for comparison.
818
			$additional_groups = array(
819
				'previous' => !empty($old_profile['additional_groups']) ? explode(',', $old_profile['additional_groups']) : array(),
820
				'new' => !empty($_POST['additional_groups']) ? array_diff($_POST['additional_groups'], array(0)) : array(),
821
			);
822
823
			sort($additional_groups['previous']);
824
			sort($additional_groups['new']);
825
826
			// What about additional groups?
827
			if ($additional_groups['previous'] != $additional_groups['new'])
828
			{
829
				foreach ($additional_groups as $type => $groups)
830
				{
831
					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...
832
					{
833
						if (isset($context['member_groups'][$group]))
834
							$additional_groups[$type][$id] = $context['member_groups'][$group]['name'];
835
						else
836
							unset($additional_groups[$type][$id]);
837
					}
838
					$additional_groups[$type] = implode(', ', $additional_groups[$type]);
839
				}
840
841
				$context['log_changes']['additional_groups'] = $additional_groups;
842
			}
843
		}
844
	}
845
846
	// @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...
847
	if ($context['user']['is_owner'])
848
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
849
	else
850
		$changeOther = allowedTo('profile_extra_any');
851
	if ($changeOther && empty($post_errors))
852
	{
853
		makeThemeChanges($context['id_member'], isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_profile['id_theme']);
854
		if (!empty($_REQUEST['sa']))
855
		{
856
			$custom_fields_errors = makeCustomFieldChanges($context['id_member'], $_REQUEST['sa'], false, true);
857
858
			if (!empty($custom_fields_errors))
859
				$post_errors = array_merge($post_errors, $custom_fields_errors);
860
		}
861
	}
862
863
	// Free memory!
864
	unset($profile_fields);
865
}
866
867
/**
868
 * Save the profile changes
869
 *
870
 * @param array &$profile_vars The items to save
871
 * @param array &$post_errors An array of information about any errors that occurred
872
 * @param int $memID The ID of the member whose profile we're saving
873
 */
874
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...
875
{
876
	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...
877
878
	// These make life easier....
879
	$old_profile = &$user_profile[$memID];
880
881
	// Permissions...
882
	if ($context['user']['is_owner'])
883
	{
884
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own', 'profile_website_any', 'profile_website_own', 'profile_signature_any', 'profile_signature_own'));
885
	}
886
	else
887
		$changeOther = allowedTo(array('profile_extra_any', 'profile_website_any', 'profile_signature_any'));
888
889
	// Arrays of all the changes - makes things easier.
890
	$profile_bools = array();
891
	$profile_ints = array();
892
	$profile_floats = array();
893
	$profile_strings = array(
894
		'buddy_list',
895
		'ignore_boards',
896
	);
897
898
	if (isset($_POST['sa']) && $_POST['sa'] == 'ignoreboards' && empty($_POST['ignore_brd']))
899
		$_POST['ignore_brd'] = array();
900
901
	unset($_POST['ignore_boards']); // Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
902
	if (isset($_POST['ignore_brd']))
903
	{
904
		if (!is_array($_POST['ignore_brd']))
905
			$_POST['ignore_brd'] = array($_POST['ignore_brd']);
906
907
		foreach ($_POST['ignore_brd'] as $k => $d)
908
		{
909
			$d = (int) $d;
910
			if ($d != 0)
911
				$_POST['ignore_brd'][$k] = $d;
912
			else
913
				unset($_POST['ignore_brd'][$k]);
914
		}
915
		$_POST['ignore_boards'] = implode(',', $_POST['ignore_brd']);
916
		unset($_POST['ignore_brd']);
917
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
	$log_changes = array();
1171
	while ($row = $smcFunc['db_fetch_assoc']($request))
1172
	{
1173
		/* This means don't save if:
1174
			- The user is NOT an admin.
1175
			- The data is not freely viewable and editable by users.
1176
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
1177
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
1178
		*/
1179
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != $user_info['id'] || $row['private'] != 2) && ($area != 'register' || $row['show_reg'] == 0))
1180
			continue;
1181
1182
		// Validate the user data.
1183
		if ($row['field_type'] == 'check')
1184
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
1185
		elseif ($row['field_type'] == 'select' || $row['field_type'] == 'radio')
1186
		{
1187
			$value = $row['default_value'];
1188
			foreach (explode(',', $row['field_options']) as $k => $v)
1189
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
1190
					$value = $v;
1191
		}
1192
		// Otherwise some form of text!
1193
		else
1194
		{
1195
			$value = isset($_POST['customfield'][$row['col_name']]) ? $_POST['customfield'][$row['col_name']] : '';
1196
1197
			if ($row['field_length'])
1198
				$value = $smcFunc['substr']($value, 0, $row['field_length']);
1199
1200
			// Any masks?
1201
			if ($row['field_type'] == 'text' && !empty($row['mask']) && $row['mask'] != 'none')
1202
			{
1203
				$value = $smcFunc['htmltrim']($value);
1204
				$valueReference = un_htmlspecialchars($value);
1205
1206
				// Try and avoid some checks. '0' could be a valid non-empty value.
1207
				if (empty($value) && !is_numeric($value))
1208
					$value = '';
1209
1210
				if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $value != filter_var($value, FILTER_SANITIZE_STRING) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
1211
				{
1212
					if ($returnErrors)
1213
						$errors[] = 'custom_field_nohtml_fail';
1214
1215
					else
1216
						$value = '';
1217
				}
1218
				elseif ($row['mask'] == 'email' && (!filter_var($value, FILTER_VALIDATE_EMAIL) || strlen($value) > 255))
1219
				{
1220
					if ($returnErrors)
1221
						$errors[] = 'custom_field_mail_fail';
1222
1223
					else
1224
						$value = '';
1225
				}
1226
				elseif ($row['mask'] == 'number')
1227
				{
1228
					$value = (int) $value;
1229
				}
1230
				elseif (substr($row['mask'], 0, 5) == 'regex' && trim($value) != '' && preg_match(substr($row['mask'], 5), $value) === 0)
1231
				{
1232
					if ($returnErrors)
1233
						$errors[] = 'custom_field_regex_fail';
1234
1235
					else
1236
						$value = '';
1237
				}
1238
1239
				unset($valueReference);
1240
			}
1241
		}
1242
1243
		// Did it change?
1244
		if (!isset($user_profile[$memID]['options'][$row['col_name']]) || $user_profile[$memID]['options'][$row['col_name']] !== $value)
1245
		{
1246
			$log_changes[] = array(
1247
				'action' => 'customfield_' . $row['col_name'],
1248
				'log_type' => 'user',
1249
				'extra' => array(
1250
					'previous' => !empty($user_profile[$memID]['options'][$row['col_name']]) ? $user_profile[$memID]['options'][$row['col_name']] : '',
1251
					'new' => $value,
1252
					'applicator' => $user_info['id'],
1253
					'member_affected' => $memID,
1254
				),
1255
			);
1256
			$changes[] = array(1, $row['col_name'], $value, $memID);
1257
			$user_profile[$memID]['options'][$row['col_name']] = $value;
1258
		}
1259
	}
1260
	$smcFunc['db_free_result']($request);
1261
1262
	$hook_errors = call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, &$errors, $returnErrors, $memID, $area, $sanitize));
1263
1264
	if (!empty($hook_errors) && is_array($hook_errors))
1265
		$errors = array_merge($errors, $hook_errors);
1266
1267
	// Make those changes!
1268
	if (!empty($changes) && empty($context['password_auth_failed']) && empty($errors))
1269
	{
1270
		$smcFunc['db_insert']('replace',
1271
			'{db_prefix}themes',
1272
			array('id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
1273
			$changes,
1274
			array('id_theme', 'variable', 'id_member')
1275
		);
1276
		if (!empty($log_changes) && !empty($modSettings['modlog_enabled']))
1277
		{
1278
			require_once($sourcedir . '/Logging.php');
1279
			logActions($log_changes);
1280
		}
1281
	}
1282
1283
	if ($returnErrors)
1284
		return $errors;
1285
}
1286
1287
/**
1288
 * Show all the users buddies, as well as a add/delete interface.
1289
 *
1290
 * @param int $memID The ID of the member
1291
 */
1292
function editBuddyIgnoreLists($memID)
1293
{
1294
	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...
1295
1296
	// Do a quick check to ensure people aren't getting here illegally!
1297
	if (!$context['user']['is_owner'] || empty($modSettings['enable_buddylist']))
1298
		fatal_lang_error('no_access', false);
1299
1300
	// Can we email the user direct?
1301
	$context['can_moderate_forum'] = allowedTo('moderate_forum');
1302
	$context['can_send_email'] = allowedTo('moderate_forum');
1303
1304
	$subActions = array(
1305
		'buddies' => array('editBuddies', $txt['editBuddies']),
1306
		'ignore' => array('editIgnoreList', $txt['editIgnoreList']),
1307
	);
1308
1309
	$context['list_area'] = isset($_GET['sa']) && isset($subActions[$_GET['sa']]) ? $_GET['sa'] : 'buddies';
1310
1311
	// Create the tabs for the template.
1312
	$context[$context['profile_menu_name']]['tab_data'] = array(
1313
		'title' => $txt['editBuddyIgnoreLists'],
1314
		'description' => $txt['buddy_ignore_desc'],
1315
		'icon' => 'profile_hd.png',
1316
		'tabs' => array(
1317
			'buddies' => array(),
1318
			'ignore' => array(),
1319
		),
1320
	);
1321
1322
	loadJavaScriptFile('suggest.js', array('defer' => false), 'smf_suggest');
1323
1324
	// Pass on to the actual function.
1325
	$context['sub_template'] = $subActions[$context['list_area']][0];
1326
	$call = call_helper($subActions[$context['list_area']][0], true);
1327
1328
	if (!empty($call))
1329
		call_user_func($call, $memID);
1330
}
1331
1332
/**
1333
 * Show all the users buddies, as well as a add/delete interface.
1334
 *
1335
 * @param int $memID The ID of the member
1336
 */
1337
function editBuddies($memID)
1338
{
1339
	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...
1340
	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...
1341
1342
	// For making changes!
1343
	$buddiesArray = explode(',', $user_profile[$memID]['buddy_list']);
1344
	foreach ($buddiesArray as $k => $dummy)
1345
		if ($dummy == '')
1346
			unset($buddiesArray[$k]);
1347
1348
	// Removing a buddy?
1349
	if (isset($_GET['remove']))
1350
	{
1351
		checkSession('get');
1352
1353
		call_integration_hook('integrate_remove_buddy', array($memID));
1354
1355
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1356
1357
		// Heh, I'm lazy, do it the easy way...
1358
		foreach ($buddiesArray as $key => $buddy)
1359
			if ($buddy == (int) $_GET['remove'])
1360
			{
1361
				unset($buddiesArray[$key]);
1362
				$_SESSION['prf-save'] = true;
1363
			}
1364
1365
		// Make the changes.
1366
		$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1367
		updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1368
1369
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1370
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1371
	}
1372
	elseif (isset($_POST['new_buddy']))
1373
	{
1374
		checkSession();
1375
1376
		// Prepare the string for extraction...
1377
		$_POST['new_buddy'] = strtr($smcFunc['htmlspecialchars']($_POST['new_buddy'], ENT_QUOTES), array('&quot;' => '"'));
1378
		preg_match_all('~"([^"]+)"~', $_POST['new_buddy'], $matches);
1379
		$new_buddies = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_buddy']))));
1380
1381 View Code Duplication
		foreach ($new_buddies as $k => $dummy)
1382
		{
1383
			$new_buddies[$k] = strtr(trim($new_buddies[$k]), array('\'' => '&#039;'));
1384
1385
			if (strlen($new_buddies[$k]) == 0 || in_array($new_buddies[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1386
				unset($new_buddies[$k]);
1387
		}
1388
1389
		call_integration_hook('integrate_add_buddies', array($memID, &$new_buddies));
1390
1391
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1392 View Code Duplication
		if (!empty($new_buddies))
1393
		{
1394
			// Now find out the id_member of the buddy.
1395
			$request = $smcFunc['db_query']('', '
1396
				SELECT id_member
1397
				FROM {db_prefix}members
1398
				WHERE member_name IN ({array_string:new_buddies}) OR real_name IN ({array_string:new_buddies})
1399
				LIMIT {int:count_new_buddies}',
1400
				array(
1401
					'new_buddies' => $new_buddies,
1402
					'count_new_buddies' => count($new_buddies),
1403
				)
1404
			);
1405
1406
			if ($smcFunc['db_num_rows']($request) != 0)
1407
				$_SESSION['prf-save'] = true;
1408
1409
			// Add the new member to the buddies array.
1410
			while ($row = $smcFunc['db_fetch_assoc']($request))
1411
			{
1412
				if (in_array($row['id_member'], $buddiesArray))
1413
					continue;
1414
				else
1415
					$buddiesArray[] = (int) $row['id_member'];
1416
			}
1417
			$smcFunc['db_free_result']($request);
1418
1419
			// Now update the current users buddy list.
1420
			$user_profile[$memID]['buddy_list'] = implode(',', $buddiesArray);
1421
			updateMemberData($memID, array('buddy_list' => $user_profile[$memID]['buddy_list']));
1422
		}
1423
1424
		// Back to the buddy list!
1425
		redirectexit('action=profile;area=lists;sa=buddies;u=' . $memID);
1426
	}
1427
1428
	// Get all the users "buddies"...
1429
	$buddies = array();
1430
1431
	// Gotta load the custom profile fields names.
1432
	$request = $smcFunc['db_query']('', '
1433
		SELECT col_name, field_name, field_desc, field_type, bbc, enclose
1434
		FROM {db_prefix}custom_fields
1435
		WHERE active = {int:active}
1436
			AND private < {int:private_level}',
1437
		array(
1438
			'active' => 1,
1439
			'private_level' => 2,
1440
		)
1441
	);
1442
1443
	$context['custom_pf'] = array();
1444
	$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...
1445
	while ($row = $smcFunc['db_fetch_assoc']($request))
1446
		if (!isset($disabled_fields[$row['col_name']]))
1447
			$context['custom_pf'][$row['col_name']] = array(
1448
				'label' => $row['field_name'],
1449
				'type' => $row['field_type'],
1450
				'bbc' => !empty($row['bbc']),
1451
				'enclose' => $row['enclose'],
1452
			);
1453
1454
	// Gotta disable the gender option.
1455
	if (isset($context['custom_pf']['cust_gender']) && $context['custom_pf']['cust_gender'] == 'None')
1456
		unset($context['custom_pf']['cust_gender']);
1457
1458
	$smcFunc['db_free_result']($request);
1459
1460 View Code Duplication
	if (!empty($buddiesArray))
1461
	{
1462
		$result = $smcFunc['db_query']('', '
1463
			SELECT id_member
1464
			FROM {db_prefix}members
1465
			WHERE id_member IN ({array_int:buddy_list})
1466
			ORDER BY real_name
1467
			LIMIT {int:buddy_list_count}',
1468
			array(
1469
				'buddy_list' => $buddiesArray,
1470
				'buddy_list_count' => substr_count($user_profile[$memID]['buddy_list'], ',') + 1,
1471
			)
1472
		);
1473
		while ($row = $smcFunc['db_fetch_assoc']($result))
1474
			$buddies[] = $row['id_member'];
1475
		$smcFunc['db_free_result']($result);
1476
	}
1477
1478
	$context['buddy_count'] = count($buddies);
1479
1480
	// Load all the members up.
1481
	loadMemberData($buddies, false, 'profile');
1482
1483
	// Setup the context for each buddy.
1484
	$context['buddies'] = array();
1485
	foreach ($buddies as $buddy)
1486
	{
1487
		loadMemberContext($buddy);
1488
		$context['buddies'][$buddy] = $memberContext[$buddy];
1489
1490
		// Make sure to load the appropriate fields for each user
1491
		if (!empty($context['custom_pf']))
1492
		{
1493
			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...
1494
			{
1495
				// Don't show anything if there isn't anything to show.
1496 View Code Duplication
				if (!isset($context['buddies'][$buddy]['options'][$key]))
1497
				{
1498
					$context['buddies'][$buddy]['options'][$key] = '';
1499
					continue;
1500
				}
1501
1502 View Code Duplication
				if ($column['bbc'] && !empty($context['buddies'][$buddy]['options'][$key]))
1503
					$context['buddies'][$buddy]['options'][$key] = strip_tags(parse_bbc($context['buddies'][$buddy]['options'][$key]));
1504
1505
				elseif ($column['type'] == 'check')
1506
					$context['buddies'][$buddy]['options'][$key] = $context['buddies'][$buddy]['options'][$key] == 0 ? $txt['no'] : $txt['yes'];
1507
1508
				// Enclosing the user input within some other text?
1509
				if (!empty($column['enclose']) && !empty($context['buddies'][$buddy]['options'][$key]))
1510
					$context['buddies'][$buddy]['options'][$key] = strtr($column['enclose'], array(
1511
						'{SCRIPTURL}' => $scripturl,
1512
						'{IMAGES_URL}' => $settings['images_url'],
1513
						'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
1514
						'{INPUT}' => $context['buddies'][$buddy]['options'][$key],
1515
					));
1516
			}
1517
		}
1518
	}
1519
1520 View Code Duplication
	if (isset($_SESSION['prf-save']))
1521
	{
1522
		if ($_SESSION['prf-save'] === true)
1523
			$context['saved_successful'] = true;
1524
		else
1525
			$context['saved_failed'] = $_SESSION['prf-save'];
1526
1527
		unset($_SESSION['prf-save']);
1528
	}
1529
1530
	call_integration_hook('integrate_view_buddies', array($memID));
1531
}
1532
1533
/**
1534
 * Allows the user to view their ignore list, as well as the option to manage members on it.
1535
 *
1536
 * @param int $memID The ID of the member
1537
 */
1538
function editIgnoreList($memID)
1539
{
1540
	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...
1541
	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...
1542
1543
	// For making changes!
1544
	$ignoreArray = explode(',', $user_profile[$memID]['pm_ignore_list']);
1545
	foreach ($ignoreArray as $k => $dummy)
1546
		if ($dummy == '')
1547
			unset($ignoreArray[$k]);
1548
1549
	// Removing a member from the ignore list?
1550
	if (isset($_GET['remove']))
1551
	{
1552
		checkSession('get');
1553
1554
		$_SESSION['prf-save'] = $txt['could_not_remove_person'];
1555
1556
		// Heh, I'm lazy, do it the easy way...
1557
		foreach ($ignoreArray as $key => $id_remove)
1558
			if ($id_remove == (int) $_GET['remove'])
1559
			{
1560
				unset($ignoreArray[$key]);
1561
				$_SESSION['prf-save'] = true;
1562
			}
1563
1564
		// Make the changes.
1565
		$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1566
		updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1567
1568
		// Redirect off the page because we don't like all this ugly query stuff to stick in the history.
1569
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1570
	}
1571
	elseif (isset($_POST['new_ignore']))
1572
	{
1573
		checkSession();
1574
		// Prepare the string for extraction...
1575
		$_POST['new_ignore'] = strtr($smcFunc['htmlspecialchars']($_POST['new_ignore'], ENT_QUOTES), array('&quot;' => '"'));
1576
		preg_match_all('~"([^"]+)"~', $_POST['new_ignore'], $matches);
1577
		$new_entries = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_POST['new_ignore']))));
1578
1579 View Code Duplication
		foreach ($new_entries as $k => $dummy)
1580
		{
1581
			$new_entries[$k] = strtr(trim($new_entries[$k]), array('\'' => '&#039;'));
1582
1583
			if (strlen($new_entries[$k]) == 0 || in_array($new_entries[$k], array($user_profile[$memID]['member_name'], $user_profile[$memID]['real_name'])))
1584
				unset($new_entries[$k]);
1585
		}
1586
1587
		$_SESSION['prf-save'] = $txt['could_not_add_person'];
1588 View Code Duplication
		if (!empty($new_entries))
1589
		{
1590
			// Now find out the id_member for the members in question.
1591
			$request = $smcFunc['db_query']('', '
1592
				SELECT id_member
1593
				FROM {db_prefix}members
1594
				WHERE member_name IN ({array_string:new_entries}) OR real_name IN ({array_string:new_entries})
1595
				LIMIT {int:count_new_entries}',
1596
				array(
1597
					'new_entries' => $new_entries,
1598
					'count_new_entries' => count($new_entries),
1599
				)
1600
			);
1601
1602
			if ($smcFunc['db_num_rows']($request) != 0)
1603
				$_SESSION['prf-save'] = true;
1604
1605
			// Add the new member to the buddies array.
1606
			while ($row = $smcFunc['db_fetch_assoc']($request))
1607
			{
1608
				if (in_array($row['id_member'], $ignoreArray))
1609
					continue;
1610
				else
1611
					$ignoreArray[] = (int) $row['id_member'];
1612
			}
1613
			$smcFunc['db_free_result']($request);
1614
1615
			// Now update the current users buddy list.
1616
			$user_profile[$memID]['pm_ignore_list'] = implode(',', $ignoreArray);
1617
			updateMemberData($memID, array('pm_ignore_list' => $user_profile[$memID]['pm_ignore_list']));
1618
		}
1619
1620
		// Back to the list of pityful people!
1621
		redirectexit('action=profile;area=lists;sa=ignore;u=' . $memID);
1622
	}
1623
1624
	// Initialise the list of members we're ignoring.
1625
	$ignored = array();
1626
1627 View Code Duplication
	if (!empty($ignoreArray))
1628
	{
1629
		$result = $smcFunc['db_query']('', '
1630
			SELECT id_member
1631
			FROM {db_prefix}members
1632
			WHERE id_member IN ({array_int:ignore_list})
1633
			ORDER BY real_name
1634
			LIMIT {int:ignore_list_count}',
1635
			array(
1636
				'ignore_list' => $ignoreArray,
1637
				'ignore_list_count' => substr_count($user_profile[$memID]['pm_ignore_list'], ',') + 1,
1638
			)
1639
		);
1640
		while ($row = $smcFunc['db_fetch_assoc']($result))
1641
			$ignored[] = $row['id_member'];
1642
		$smcFunc['db_free_result']($result);
1643
	}
1644
1645
	$context['ignore_count'] = count($ignored);
1646
1647
	// Load all the members up.
1648
	loadMemberData($ignored, false, 'profile');
1649
1650
	// Setup the context for each buddy.
1651
	$context['ignore_list'] = array();
1652
	foreach ($ignored as $ignore_member)
1653
	{
1654
		loadMemberContext($ignore_member);
1655
		$context['ignore_list'][$ignore_member] = $memberContext[$ignore_member];
1656
	}
1657
1658 View Code Duplication
	if (isset($_SESSION['prf-save']))
1659
	{
1660
		if ($_SESSION['prf-save'] === true)
1661
			$context['saved_successful'] = true;
1662
		else
1663
			$context['saved_failed'] = $_SESSION['prf-save'];
1664
1665
		unset($_SESSION['prf-save']);
1666
	}
1667
}
1668
1669
/**
1670
 * Handles the account section of the profile
1671
 *
1672
 * @param int $memID The ID of the member
1673
 */
1674
function account($memID)
1675
{
1676
	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...
1677
1678
	loadThemeOptions($memID);
1679
	if (allowedTo(array('profile_identity_own', 'profile_identity_any', 'profile_password_own', 'profile_password_any')))
1680
		loadCustomFields($memID, 'account');
1681
1682
	$context['sub_template'] = 'edit_options';
1683
	$context['page_desc'] = $txt['account_info'];
1684
1685
	setupProfileContext(
1686
		array(
1687
			'member_name', 'real_name', 'date_registered', 'posts', 'lngfile', 'hr',
1688
			'id_group', 'hr',
1689
			'email_address', 'show_online', 'hr',
1690
			'tfa', 'hr',
1691
			'passwrd1', 'passwrd2', 'hr',
1692
			'secret_question', 'secret_answer',
1693
		)
1694
	);
1695
}
1696
1697
/**
1698
 * Handles the main "Forum Profile" section of the profile
1699
 *
1700
 * @param int $memID The ID of the member
1701
 */
1702
function forumProfile($memID)
1703
{
1704
	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...
1705
1706
	loadThemeOptions($memID);
1707
	if (allowedTo(array('profile_forum_own', 'profile_forum_any')))
1708
		loadCustomFields($memID, 'forumprofile');
1709
1710
	$context['sub_template'] = 'edit_options';
1711
	$context['page_desc'] = $txt['forumProfile_info'];
1712
	$context['show_preview_button'] = true;
1713
1714
	setupProfileContext(
1715
		array(
1716
			'avatar_choice', 'hr', 'personal_text', 'hr',
1717
			'bday1', 'usertitle', 'signature', 'hr',
1718
			'website_title', 'website_url',
1719
		)
1720
	);
1721
}
1722
1723
/**
1724
 * Recursive function to retrieve server-stored avatar files
1725
 *
1726
 * @param string $directory The directory to look for files in
1727
 * @param int $level How many levels we should go in the directory
1728
 * @return array An array of information about the files and directories found
1729
 */
1730
function getAvatars($directory, $level)
1731
{
1732
	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...
1733
1734
	$result = array();
1735
1736
	// Open the directory..
1737
	$dir = dir($modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory);
1738
	$dirs = array();
1739
	$files = array();
1740
1741
	if (!$dir)
1742
		return array();
1743
1744
	while ($line = $dir->read())
1745
	{
1746
		if (in_array($line, array('.', '..', 'blank.png', 'index.php')))
1747
			continue;
1748
1749
		if (is_dir($modSettings['avatar_directory'] . '/' . $directory . (!empty($directory) ? '/' : '') . $line))
1750
			$dirs[] = $line;
1751
		else
1752
			$files[] = $line;
1753
	}
1754
	$dir->close();
1755
1756
	// Sort the results...
1757
	natcasesort($dirs);
1758
	natcasesort($files);
1759
1760
	if ($level == 0)
1761
	{
1762
		$result[] = array(
1763
			'filename' => 'blank.png',
1764
			'checked' => in_array($context['member']['avatar']['server_pic'], array('', 'blank.png')),
1765
			'name' => $txt['no_pic'],
1766
			'is_dir' => false
1767
		);
1768
	}
1769
1770
	foreach ($dirs as $line)
1771
	{
1772
		$tmp = getAvatars($directory . (!empty($directory) ? '/' : '') . $line, $level + 1);
1773
		if (!empty($tmp))
1774
			$result[] = array(
1775
				'filename' => $smcFunc['htmlspecialchars']($line),
1776
				'checked' => strpos($context['member']['avatar']['server_pic'], $line . '/') !== false,
1777
				'name' => '[' . $smcFunc['htmlspecialchars'](str_replace('_', ' ', $line)) . ']',
1778
				'is_dir' => true,
1779
				'files' => $tmp
1780
		);
1781
		unset($tmp);
1782
	}
1783
1784
	foreach ($files as $line)
1785
	{
1786
		$filename = substr($line, 0, (strlen($line) - strlen(strrchr($line, '.'))));
1787
		$extension = substr(strrchr($line, '.'), 1);
1788
1789
		// Make sure it is an image.
1790
		if (strcasecmp($extension, 'gif') != 0 && strcasecmp($extension, 'jpg') != 0 && strcasecmp($extension, 'jpeg') != 0 && strcasecmp($extension, 'png') != 0 && strcasecmp($extension, 'bmp') != 0)
1791
			continue;
1792
1793
		$result[] = array(
1794
			'filename' => $smcFunc['htmlspecialchars']($line),
1795
			'checked' => $line == $context['member']['avatar']['server_pic'],
1796
			'name' => $smcFunc['htmlspecialchars'](str_replace('_', ' ', $filename)),
1797
			'is_dir' => false
1798
		);
1799
		if ($level == 1)
1800
			$context['avatar_list'][] = $directory . '/' . $line;
1801
	}
1802
1803
	return $result;
1804
}
1805
1806
/**
1807
 * Handles the "Look and Layout" section of the profile
1808
 *
1809
 * @param int $memID The ID of the member
1810
 */
1811
function theme($memID)
1812
{
1813
	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...
1814
1815
	loadTemplate('Settings');
1816
	loadSubTemplate('options');
1817
1818
	// Let mods hook into the theme options.
1819
	call_integration_hook('integrate_theme_options');
1820
1821
	loadThemeOptions($memID);
1822
	if (allowedTo(array('profile_extra_own', 'profile_extra_any')))
1823
		loadCustomFields($memID, 'theme');
1824
1825
	$context['sub_template'] = 'edit_options';
1826
	$context['page_desc'] = $txt['theme_info'];
1827
1828
	setupProfileContext(
1829
		array(
1830
			'id_theme', 'smiley_set', 'hr',
1831
			'time_format', 'timezone', 'hr',
1832
			'theme_settings',
1833
		)
1834
	);
1835
}
1836
1837
/**
1838
 * Display the notifications and settings for changes.
1839
 *
1840
 * @param int $memID The ID of the member
1841
 */
1842
function notification($memID)
1843
{
1844
	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...
1845
1846
	// Going to want this for consistency.
1847
	loadCSSFile('admin.css', array(), 'smf_admin');
1848
1849
	// This is just a bootstrap for everything else.
1850
	$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...
1851
		'alerts' => 'alert_configuration',
1852
		'markread' => 'alert_markread',
1853
		'topics' => 'alert_notifications_topics',
1854
		'boards' => 'alert_notifications_boards',
1855
	);
1856
1857
	$subAction = !empty($_GET['sa']) && isset($sa[$_GET['sa']]) ? $_GET['sa'] : 'alerts';
1858
1859
	$context['sub_template'] = $sa[$subAction];
1860
	$context[$context['profile_menu_name']]['tab_data'] = array(
1861
		'title' => $txt['notification'],
1862
		'help' => '',
1863
		'description' => $txt['notification_info'],
1864
	);
1865
	$sa[$subAction]($memID);
1866
}
1867
1868
/**
1869
 * Handles configuration of alert preferences
1870
 *
1871
 * @param int $memID The ID of the member
1872
 */
1873
function alert_configuration($memID)
1874
{
1875
	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...
1876
1877
	if (!isset($context['token_check']))
1878
		$context['token_check'] = 'profile-nt' . $memID;
1879
1880
	is_not_guest();
1881
	if (!$context['user']['is_owner'])
1882
		isAllowedTo('profile_extra_any');
1883
1884
	// Set the post action if we're coming from the profile...
1885
	if (!isset($context['action']))
1886
		$context['action'] = 'action=profile;area=notification;sa=alerts;u=' . $memID;
1887
1888
	// What options are set
1889
	loadThemeOptions($memID);
1890
	loadJavaScriptFile('alertSettings.js', array(), 'smf_alertSettings');
1891
1892
	// Now load all the values for this user.
1893
	require_once($sourcedir . '/Subs-Notify.php');
1894
	$prefs = getNotifyPrefs($memID, '', $memID != 0);
1895
1896
	$context['alert_prefs'] = !empty($prefs[$memID]) ? $prefs[$memID] : array();
1897
1898
	$context['member'] += array(
1899
		'alert_timeout' => isset($context['alert_prefs']['alert_timeout']) ? $context['alert_prefs']['alert_timeout'] : 10,
1900
		'notify_announcements' => isset($context['alert_prefs']['announcements']) ? $context['alert_prefs']['announcements'] : 0,
1901
	);
1902
1903
	// Now for the exciting stuff.
1904
	// We have groups of items, each item has both an alert and an email key as well as an optional help string.
1905
	// Valid values for these keys are 'always', 'yes', 'never'; if using always or never you should add a help string.
1906
	$alert_types = array(
1907
		'board' => array(
1908
			'topic_notify' => array('alert' => 'yes', 'email' => 'yes'),
1909
			'board_notify' => array('alert' => 'yes', 'email' => 'yes'),
1910
		),
1911
		'msg' => array(
1912
			'msg_mention' => array('alert' => 'yes', 'email' => 'yes'),
1913
			'msg_quote' => array('alert' => 'yes', 'email' => 'yes'),
1914
			'msg_like' => array('alert' => 'yes', 'email' => 'never'),
1915
			'unapproved_reply' => array('alert' => 'yes', 'email' => 'yes'),
1916
		),
1917
		'pm' => array(
1918
			'pm_new' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_read', 'is_board' => false)),
1919
			'pm_reply' => array('alert' => 'never', 'email' => 'yes', 'help' => 'alert_pm_new', 'permission' => array('name' => 'pm_send', 'is_board' => false)),
1920
		),
1921
		'groupr' => array(
1922
			'groupr_approved' => array('alert' => 'always', 'email' => 'yes'),
1923
			'groupr_rejected' => array('alert' => 'always', 'email' => 'yes'),
1924
		),
1925
		'moderation' => array(
1926
			'unapproved_post' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'approve_posts', 'is_board' => true)),
1927
			'msg_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1928
			'msg_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_board', 'is_board' => true)),
1929
			'member_report' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1930
			'member_report_reply' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1931
		),
1932
		'members' => array(
1933
			'member_register' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'moderate_forum', 'is_board' => false)),
1934
			'request_group' => array('alert' => 'yes', 'email' => 'yes'),
1935
			'warn_any' => array('alert' => 'yes', 'email' => 'yes', 'permission' => array('name' => 'issue_warning', 'is_board' => false)),
1936
			'buddy_request'  => array('alert' => 'yes', 'email' => 'never'),
1937
			'birthday'  => array('alert' => 'yes', 'email' => 'yes'),
1938
		),
1939
		'calendar' => array(
1940
			'event_new' => array('alert' => 'yes', 'email' => 'yes', 'help' => 'alert_event_new'),
1941
		),
1942
		'paidsubs' => array(
1943
			'paidsubs_expiring' => array('alert' => 'yes', 'email' => 'yes'),
1944
		),
1945
	);
1946
	$group_options = array(
1947
		'board' => array(
1948
			array('check', 'msg_auto_notify', 'label' => 'after'),
1949
			array('check', 'msg_receive_body', 'label' => 'after'),
1950
			array('select', 'msg_notify_pref', 'label' => 'before', 'opts' => array(
1951
				0 => $txt['alert_opt_msg_notify_pref_nothing'],
1952
				1 => $txt['alert_opt_msg_notify_pref_instant'],
1953
				2 => $txt['alert_opt_msg_notify_pref_first'],
1954
				3 => $txt['alert_opt_msg_notify_pref_daily'],
1955
				4 => $txt['alert_opt_msg_notify_pref_weekly'],
1956
			)),
1957
			array('select', 'msg_notify_type', 'label' => 'before', 'opts' => array(
1958
				1 => $txt['notify_send_type_everything'],
1959
				2 => $txt['notify_send_type_everything_own'],
1960
				3 => $txt['notify_send_type_only_replies'],
1961
				4 => $txt['notify_send_type_nothing'],
1962
			)),
1963
		),
1964
		'pm' => array(
1965
			array('select', 'pm_notify', 'label' => 'before', 'opts' => array(
1966
				1 => $txt['email_notify_all'],
1967
				2 => $txt['email_notify_buddies'],
1968
			)),
1969
		),
1970
	);
1971
1972
	// There are certain things that are disabled at the group level.
1973
	if (empty($modSettings['cal_enabled']))
1974
		unset($alert_types['calendar']);
1975
1976
	// Disable paid subscriptions at group level if they're disabled
1977
	if (empty($modSettings['paid_enabled']))
1978
		unset($alert_types['paidsubs']);
1979
1980
	// Disable membergroup requests at group level if they're disabled
1981
	if (empty($modSettings['show_group_membership']))
1982
		unset($alert_types['groupr'], $alert_types['members']['request_group']);
1983
1984
	// Disable mentions if they're disabled
1985
	if (empty($modSettings['enable_mentions']))
1986
		unset($alert_types['msg']['msg_mention']);
1987
1988
	// Disable likes if they're disabled
1989
	if (empty($modSettings['enable_likes']))
1990
		unset($alert_types['msg']['msg_like']);
1991
1992
	// Disable buddy requests if they're disabled
1993
	if (empty($modSettings['enable_buddylist']))
1994
		unset($alert_types['members']['buddy_request']);
1995
1996
	// Now, now, we could pass this through global but we should really get into the habit of
1997
	// passing content to hooks, not expecting hooks to splatter everything everywhere.
1998
	call_integration_hook('integrate_alert_types', array(&$alert_types, &$group_options));
1999
2000
	// Now we have to do some permissions testing - but only if we're not loading this from the admin center
2001
	if (!empty($memID))
2002
	{
2003
		require_once($sourcedir . '/Subs-Members.php');
2004
		$perms_cache = array();
2005
		$request = $smcFunc['db_query']('', '
2006
			SELECT COUNT(*)
2007
			FROM {db_prefix}group_moderators
2008
			WHERE id_member = {int:memID}',
2009
			array(
2010
				'memID' => $memID,
2011
			)
2012
		);
2013
2014
		list ($can_mod) = $smcFunc['db_fetch_row']($request);
2015
2016
		if (!isset($perms_cache['manage_membergroups']))
2017
		{
2018
			$members = membersAllowedTo('manage_membergroups');
2019
			$perms_cache['manage_membergroups'] = in_array($memID, $members);
2020
		}
2021
2022
		if (!($perms_cache['manage_membergroups'] || $can_mod != 0))
2023
			unset($alert_types['members']['request_group']);
2024
2025
		foreach ($alert_types as $group => $items)
2026
		{
2027
			foreach ($items as $alert_key => $alert_value)
2028
			{
2029
				if (!isset($alert_value['permission']))
2030
					continue;
2031
				if (!isset($perms_cache[$alert_value['permission']['name']]))
2032
				{
2033
					$in_board = !empty($alert_value['permission']['is_board']) ? 0 : null;
2034
					$members = membersAllowedTo($alert_value['permission']['name'], $in_board);
2035
					$perms_cache[$alert_value['permission']['name']] = in_array($memID, $members);
2036
				}
2037
2038
				if (!$perms_cache[$alert_value['permission']['name']])
2039
					unset ($alert_types[$group][$alert_key]);
2040
			}
2041
2042
			if (empty($alert_types[$group]))
2043
				unset ($alert_types[$group]);
2044
		}
2045
	}
2046
2047
	// And finally, exporting it to be useful later.
2048
	$context['alert_types'] = $alert_types;
2049
	$context['alert_group_options'] = $group_options;
2050
2051
	$context['alert_bits'] = array(
2052
		'alert' => 0x01,
2053
		'email' => 0x02,
2054
	);
2055
2056
	if (isset($_POST['notify_submit']))
2057
	{
2058
		checkSession();
2059
		validateToken($context['token_check'], 'post');
2060
2061
		// We need to step through the list of valid settings and figure out what the user has set.
2062
		$update_prefs = array();
2063
2064
		// Now the group level options
2065
		foreach ($context['alert_group_options'] as $opt_group => $group)
2066
		{
2067
			foreach ($group as $this_option)
2068
			{
2069
				switch ($this_option[0])
2070
				{
2071
					case 'check':
2072
						$update_prefs[$this_option[1]] = !empty($_POST['opt_' . $this_option[1]]) ? 1 : 0;
2073
						break;
2074
					case 'select':
2075
						if (isset($_POST['opt_' . $this_option[1]], $this_option['opts'][$_POST['opt_' . $this_option[1]]]))
2076
							$update_prefs[$this_option[1]] = $_POST['opt_' . $this_option[1]];
2077
						else
2078
						{
2079
							// We didn't have a sane value. Let's grab the first item from the possibles.
2080
							$keys = array_keys($this_option['opts']);
2081
							$first = array_shift($keys);
2082
							$update_prefs[$this_option[1]] = $first;
2083
						}
2084
						break;
2085
				}
2086
			}
2087
		}
2088
2089
		// Now the individual options
2090
		foreach ($context['alert_types'] as $alert_group => $items)
2091
		{
2092
			foreach ($items as $item_key => $this_options)
2093
			{
2094
				$this_value = 0;
2095
				foreach ($context['alert_bits'] as $type => $bitvalue)
2096
				{
2097
					if ($this_options[$type] == 'yes' && !empty($_POST[$type . '_' . $item_key]) || $this_options[$type] == 'always')
2098
						$this_value |= $bitvalue;
2099
				}
2100
				if (!isset($context['alert_prefs'][$item_key]) || $context['alert_prefs'][$item_key] != $this_value)
2101
					$update_prefs[$item_key] = $this_value;
2102
			}
2103
		}
2104
2105
		if (!empty($_POST['opt_alert_timeout']))
2106
			$update_prefs['alert_timeout'] = $context['member']['alert_timeout'] = (int) $_POST['opt_alert_timeout'];
2107
2108
		if (!empty($_POST['notify_announcements']))
2109
			$update_prefs['announcements'] = $context['member']['notify_announcements'] = (int) $_POST['notify_announcements'];
2110
2111
		setNotifyPrefs((int) $memID, $update_prefs);
2112
		foreach ($update_prefs as $pref => $value)
2113
			$context['alert_prefs'][$pref] = $value;
2114
2115
		makeNotificationChanges($memID);
2116
2117
		$context['profile_updated'] = $txt['profile_updated_own'];
2118
	}
2119
2120
	createToken($context['token_check'], 'post');
2121
}
2122
2123
/**
2124
 * Marks all alerts as read for the specified user
2125
 *
2126
 * @param int $memID The ID of the member
2127
 */
2128
function alert_markread($memID)
2129
{
2130
	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...
2131
2132
	// We do not want to output debug information here.
2133
	$db_show_debug = false;
2134
2135
	// We only want to output our little layer here.
2136
	$context['template_layers'] = array();
2137
	$context['sub_template'] = 'alerts_all_read';
2138
2139
	loadLanguage('Alerts');
2140
2141
	// Now we're all set up.
2142
	is_not_guest();
2143
	if (!$context['user']['is_owner'])
2144
		fatal_error('no_access');
2145
2146
	checkSession('get');
2147
2148
	// Assuming we're here, mark everything as read and head back.
2149
	// We only spit back the little layer because this should be called AJAXively.
2150
	$smcFunc['db_query']('', '
2151
		UPDATE {db_prefix}user_alerts
2152
		SET is_read = {int:now}
2153
		WHERE id_member = {int:current_member}
2154
			AND is_read = 0',
2155
		array(
2156
			'now' => time(),
2157
			'current_member' => $memID,
2158
		)
2159
	);
2160
2161
	updateMemberData($memID, array('alerts' => 0));
2162
}
2163
2164
/**
2165
 * Marks a group of alerts as un/read
2166
 *
2167
 * @param int $memID The user ID.
2168
 * @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.
2169
 * @param integer $read To mark as read or unread, 1 for read, 0 or any other value different than 1 for unread.
2170
 * @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...
2171
 */
2172
function alert_mark($memID, $toMark, $read = 0)
2173
{
2174
	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...
2175
2176
	if (empty($toMark) || empty($memID))
2177
		return false;
2178
2179
	$toMark = (array) $toMark;
2180
2181
	$smcFunc['db_query']('', '
2182
		UPDATE {db_prefix}user_alerts
2183
		SET is_read = {int:read}
2184
		WHERE id_alert IN({array_int:toMark})',
2185
		array(
2186
			'read' => $read == 1 ? time() : 0,
2187
			'toMark' => $toMark,
2188
		)
2189
	);
2190
2191
	// Gotta know how many unread alerts are left.
2192
	$count = alert_count($memID, true);
2193
2194
	updateMemberData($memID, array('alerts' => $count));
2195
2196
	// Might want to know this.
2197
	return $count;
2198
}
2199
2200
/**
2201
 * Deletes a single or a group of alerts by ID
2202
 *
2203
 * @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.
2204
 * @param bool|int $memID The user ID. Used to update the user unread alerts count.
2205
 * @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...
2206
 */
2207
function alert_delete($toDelete, $memID = false)
2208
{
2209
	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...
2210
2211
	if (empty($toDelete))
2212
		return false;
2213
2214
	$toDelete = (array) $toDelete;
2215
2216
	$smcFunc['db_query']('', '
2217
		DELETE FROM {db_prefix}user_alerts
2218
		WHERE id_alert IN({array_int:toDelete})',
2219
		array(
2220
			'toDelete' => $toDelete,
2221
		)
2222
	);
2223
2224
	// Gotta know how many unread alerts are left.
2225
	if ($memID)
2226
	{
2227
		$count = alert_count($memID, true);
0 ignored issues
show
Bug introduced by
It seems like $memID defined by parameter $memID on line 2207 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...
2228
2229
		updateMemberData($memID, array('alerts' => $count));
2230
2231
		// Might want to know this.
2232
		return $count;
2233
	}
2234
}
2235
2236
/**
2237
 * Counts how many alerts a user has - either unread or all depending on $unread
2238
 *
2239
 * @param int $memID The user ID.
2240
 * @param bool $unread Whether to only count unread alerts.
2241
 * @return int The number of requested alerts
2242
 */
2243
function alert_count($memID, $unread = false)
2244
{
2245
	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...
2246
2247
	if (empty($memID))
2248
		return false;
2249
2250
	$request = $smcFunc['db_query']('', '
2251
		SELECT id_alert
2252
		FROM {db_prefix}user_alerts
2253
		WHERE id_member = {int:id_member}
2254
			'.($unread ? '
2255
			AND is_read = 0' : ''),
2256
		array(
2257
			'id_member' => $memID,
2258
		)
2259
	);
2260
2261
	$count = $smcFunc['db_num_rows']($request);
2262
	$smcFunc['db_free_result']($request);
2263
2264
	return $count;
2265
}
2266
2267
/**
2268
 * Handles alerts related to topics and posts
2269
 *
2270
 * @param int $memID The ID of the member
2271
 */
2272
function alert_notifications_topics($memID)
2273
{
2274
	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...
2275
2276
	// Because of the way this stuff works, we want to do this ourselves.
2277 View Code Duplication
	if (isset($_POST['edit_notify_topics']) || isset($_POST['remove_notify_topics']))
2278
	{
2279
		checkSession();
2280
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2281
2282
		makeNotificationChanges($memID);
2283
		$context['profile_updated'] = $txt['profile_updated_own'];
2284
	}
2285
2286
	// Now set up for the token check.
2287
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2288
	createToken($context['token_check'], 'post');
2289
2290
	// Gonna want this for the list.
2291
	require_once($sourcedir . '/Subs-List.php');
2292
2293
	// Do the topic notifications.
2294
	$listOptions = array(
2295
		'id' => 'topic_notification_list',
2296
		'width' => '100%',
2297
		'items_per_page' => $modSettings['defaultMaxListItems'],
2298
		'no_items_label' => $txt['notifications_topics_none'] . '<br><br>' . $txt['notifications_topics_howto'],
2299
		'no_items_align' => 'left',
2300
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=topics',
2301
		'default_sort_col' => 'last_post',
2302
		'get_items' => array(
2303
			'function' => 'list_getTopicNotifications',
2304
			'params' => array(
2305
				$memID,
2306
			),
2307
		),
2308
		'get_count' => array(
2309
			'function' => 'list_getTopicNotificationCount',
2310
			'params' => array(
2311
				$memID,
2312
			),
2313
		),
2314
		'columns' => array(
2315
			'subject' => array(
2316
				'header' => array(
2317
					'value' => $txt['notifications_topics'],
2318
					'class' => 'lefttext',
2319
				),
2320
				'data' => array(
2321
					'function' => function($topic) use ($txt)
2322
					{
2323
						$link = $topic['link'];
2324
2325
						if ($topic['new'])
2326
							$link .= ' <a href="' . $topic['new_href'] . '"><span class="new_posts">' . $txt['new'] . '</span></a>';
2327
2328
						$link .= '<br><span class="smalltext"><em>' . $txt['in'] . ' ' . $topic['board_link'] . '</em></span>';
2329
2330
						return $link;
2331
					},
2332
				),
2333
				'sort' => array(
2334
					'default' => 'ms.subject',
2335
					'reverse' => 'ms.subject DESC',
2336
				),
2337
			),
2338
			'started_by' => array(
2339
				'header' => array(
2340
					'value' => $txt['started_by'],
2341
					'class' => 'lefttext',
2342
				),
2343
				'data' => array(
2344
					'db' => 'poster_link',
2345
				),
2346
				'sort' => array(
2347
					'default' => 'real_name_col',
2348
					'reverse' => 'real_name_col DESC',
2349
				),
2350
			),
2351
			'last_post' => array(
2352
				'header' => array(
2353
					'value' => $txt['last_post'],
2354
					'class' => 'lefttext',
2355
				),
2356
				'data' => array(
2357
					'sprintf' => array(
2358
						'format' => '<span class="smalltext">%1$s<br>' . $txt['by'] . ' %2$s</span>',
2359
						'params' => array(
2360
							'updated' => false,
2361
							'poster_updated_link' => false,
2362
						),
2363
					),
2364
				),
2365
				'sort' => array(
2366
					'default' => 'ml.id_msg DESC',
2367
					'reverse' => 'ml.id_msg',
2368
				),
2369
			),
2370
			'alert' => array(
2371
				'header' => array(
2372
					'value' => $txt['notify_what_how'],
2373
					'class' => 'lefttext',
2374
				),
2375
				'data' => array(
2376
					'function' => function($topic) use ($txt)
2377
					{
2378
						$pref = $topic['notify_pref'];
2379
						$mode = !empty($topic['unwatched']) ? 0 : ($pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1));
2380
						return $txt['notify_topic_' . $mode];
2381
					},
2382
				),
2383
			),
2384
			'delete' => array(
2385
				'header' => array(
2386
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2387
					'style' => 'width: 4%;',
2388
					'class' => 'centercol',
2389
				),
2390
				'data' => array(
2391
					'sprintf' => array(
2392
						'format' => '<input type="checkbox" name="notify_topics[]" value="%1$d">',
2393
						'params' => array(
2394
							'id' => false,
2395
						),
2396
					),
2397
					'class' => 'centercol',
2398
				),
2399
			),
2400
		),
2401
		'form' => array(
2402
			'href' => $scripturl . '?action=profile;area=notification;sa=topics',
2403
			'include_sort' => true,
2404
			'include_start' => true,
2405
			'hidden_fields' => array(
2406
				'u' => $memID,
2407
				'sa' => $context['menu_item_selected'],
2408
				$context['session_var'] => $context['session_id'],
2409
			),
2410
			'token' => $context['token_check'],
2411
		),
2412
		'additional_rows' => array(
2413
			array(
2414
				'position' => 'bottom_of_list',
2415
				'value' => '<input type="submit" name="edit_notify_topics" value="' . $txt['notifications_update'] . '" class="button" />
2416
							<input type="submit" name="remove_notify_topics" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2417
				'class' => 'floatright',
2418
			),
2419
		),
2420
	);
2421
2422
	// Create the notification list.
2423
	createList($listOptions);
2424
}
2425
2426
/**
2427
 * Handles preferences related to board-level notifications
2428
 *
2429
 * @param int $memID The ID of the member
2430
 */
2431
function alert_notifications_boards($memID)
2432
{
2433
	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...
2434
2435
	// Because of the way this stuff works, we want to do this ourselves.
2436 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...
2437
	{
2438
		checkSession();
2439
		validateToken(str_replace('%u', $memID, 'profile-nt%u'), 'post');
2440
2441
		makeNotificationChanges($memID);
2442
		$context['profile_updated'] = $txt['profile_updated_own'];
2443
	}
2444
2445
	// Now set up for the token check.
2446
	$context['token_check'] = str_replace('%u', $memID, 'profile-nt%u');
2447
	createToken($context['token_check'], 'post');
2448
2449
	// Gonna want this for the list.
2450
	require_once($sourcedir . '/Subs-List.php');
2451
2452
	// Fine, start with the board list.
2453
	$listOptions = array(
2454
		'id' => 'board_notification_list',
2455
		'width' => '100%',
2456
		'no_items_label' => $txt['notifications_boards_none'] . '<br><br>' . $txt['notifications_boards_howto'],
2457
		'no_items_align' => 'left',
2458
		'base_href' => $scripturl . '?action=profile;u=' . $memID . ';area=notification;sa=boards',
2459
		'default_sort_col' => 'board_name',
2460
		'get_items' => array(
2461
			'function' => 'list_getBoardNotifications',
2462
			'params' => array(
2463
				$memID,
2464
			),
2465
		),
2466
		'columns' => array(
2467
			'board_name' => array(
2468
				'header' => array(
2469
					'value' => $txt['notifications_boards'],
2470
					'class' => 'lefttext',
2471
				),
2472
				'data' => array(
2473
					'function' => function($board) use ($txt)
2474
					{
2475
						$link = $board['link'];
2476
2477
						if ($board['new'])
2478
							$link .= ' <a href="' . $board['href'] . '"><span class="new_posts">' . $txt['new'] . '</span></a>';
2479
2480
						return $link;
2481
					},
2482
				),
2483
				'sort' => array(
2484
					'default' => 'name',
2485
					'reverse' => 'name DESC',
2486
				),
2487
			),
2488
			'alert' => array(
2489
				'header' => array(
2490
					'value' => $txt['notify_what_how'],
2491
					'class' => 'lefttext',
2492
				),
2493
				'data' => array(
2494
					'function' => function($board) use ($txt)
2495
					{
2496
						$pref = $board['notify_pref'];
2497
						$mode = $pref & 0x02 ? 3 : ($pref & 0x01 ? 2 : 1);
2498
						return $txt['notify_board_' . $mode];
2499
					},
2500
				),
2501
			),
2502
			'delete' => array(
2503
				'header' => array(
2504
					'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
2505
					'style' => 'width: 4%;',
2506
					'class' => 'centercol',
2507
				),
2508
				'data' => array(
2509
					'sprintf' => array(
2510
						'format' => '<input type="checkbox" name="notify_boards[]" value="%1$d">',
2511
						'params' => array(
2512
							'id' => false,
2513
						),
2514
					),
2515
					'class' => 'centercol',
2516
				),
2517
			),
2518
		),
2519
		'form' => array(
2520
			'href' => $scripturl . '?action=profile;area=notification;sa=boards',
2521
			'include_sort' => true,
2522
			'include_start' => true,
2523
			'hidden_fields' => array(
2524
				'u' => $memID,
2525
				'sa' => $context['menu_item_selected'],
2526
				$context['session_var'] => $context['session_id'],
2527
			),
2528
			'token' => $context['token_check'],
2529
		),
2530
		'additional_rows' => array(
2531
			array(
2532
				'position' => 'bottom_of_list',
2533
				'value' => '<input type="submit" name="edit_notify_boards" value="' . $txt['notifications_update'] . '" class="button">
2534
							<input type="submit" name="remove_notify_boards" value="' . $txt['notification_remove_pref'] . '" class="button" />',
2535
				'class' => 'floatright',
2536
			),
2537
		),
2538
	);
2539
2540
	// Create the board notification list.
2541
	createList($listOptions);
2542
}
2543
2544
/**
2545
 * Determins how many topics a user has requested notifications for
2546
 *
2547
 * @param int $memID The ID of the member
2548
 * @return int The number of topic notifications for this user
2549
 */
2550
function list_getTopicNotificationCount($memID)
2551
{
2552
	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...
2553
2554
	$request = $smcFunc['db_query']('', '
2555
		SELECT COUNT(*)
2556
		FROM {db_prefix}log_notify AS ln' . (!$modSettings['postmod_active'] && $user_info['query_see_board'] === '1=1' ? '' : '
2557
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)') . ($user_info['query_see_board'] === '1=1' ? '' : '
2558
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)') . '
2559
		WHERE ln.id_member = {int:selected_member}' . ($user_info['query_see_board'] === '1=1' ? '' : '
2560
			AND {query_see_board}') . ($modSettings['postmod_active'] ? '
2561
			AND t.approved = {int:is_approved}' : ''),
2562
		array(
2563
			'selected_member' => $memID,
2564
			'is_approved' => 1,
2565
		)
2566
	);
2567
	list ($totalNotifications) = $smcFunc['db_fetch_row']($request);
2568
	$smcFunc['db_free_result']($request);
2569
2570
	return (int) $totalNotifications;
2571
}
2572
2573
/**
2574
 * Gets information about all the topics a user has requested notifications for. Callback for the list in alert_notifications_topics
2575
 *
2576
 * @param int $start Which item to start with (for pagination purposes)
2577
 * @param int $items_per_page How many items to display on each page
2578
 * @param string $sort A string indicating how to sort the results
2579
 * @param int $memID The ID of the member
2580
 * @return array An array of information about the topics a user has subscribed to
2581
 */
2582
function list_getTopicNotifications($start, $items_per_page, $sort, $memID)
2583
{
2584
	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...
2585
2586
	require_once($sourcedir . '/Subs-Notify.php');
2587
	$prefs = getNotifyPrefs($memID);
2588
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2589
2590
	// All the topics with notification on...
2591
	$request = $smcFunc['db_query']('', '
2592
		SELECT
2593
			COALESCE(lt.id_msg, COALESCE(lmr.id_msg, -1)) + 1 AS new_from, b.id_board, b.name,
2594
			t.id_topic, ms.subject, ms.id_member, COALESCE(mem.real_name, ms.poster_name) AS real_name_col,
2595
			ml.id_msg_modified, ml.poster_time, ml.id_member AS id_member_updated,
2596
			COALESCE(mem2.real_name, ml.poster_name) AS last_real_name,
2597
			lt.unwatched
2598
		FROM {db_prefix}log_notify AS ln
2599
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
2600
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})
2601
			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
2602
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
2603
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ms.id_member)
2604
			LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = ml.id_member)
2605
			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
2606
			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})
2607
		WHERE ln.id_member = {int:selected_member}
2608
		ORDER BY {raw:sort}
2609
		LIMIT {int:offset}, {int:items_per_page}',
2610
		array(
2611
			'current_member' => $user_info['id'],
2612
			'is_approved' => 1,
2613
			'selected_member' => $memID,
2614
			'sort' => $sort,
2615
			'offset' => $start,
2616
			'items_per_page' => $items_per_page,
2617
		)
2618
	);
2619
	$notification_topics = array();
2620
	while ($row = $smcFunc['db_fetch_assoc']($request))
2621
	{
2622
		censorText($row['subject']);
2623
2624
		$notification_topics[] = array(
2625
			'id' => $row['id_topic'],
2626
			'poster_link' => empty($row['id_member']) ? $row['real_name_col'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name_col'] . '</a>',
2627
			'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>',
2628
			'subject' => $row['subject'],
2629
			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
2630
			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
2631
			'new' => $row['new_from'] <= $row['id_msg_modified'],
2632
			'new_from' => $row['new_from'],
2633
			'updated' => timeformat($row['poster_time']),
2634
			'new_href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new',
2635
			'new_link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['new_from'] . '#new">' . $row['subject'] . '</a>',
2636
			'board_link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2637
			'notify_pref' => isset($prefs['topic_notify_' . $row['id_topic']]) ? $prefs['topic_notify_' . $row['id_topic']] : (!empty($prefs['topic_notify']) ? $prefs['topic_notify'] : 0),
2638
			'unwatched' => $row['unwatched'],
2639
		);
2640
	}
2641
	$smcFunc['db_free_result']($request);
2642
2643
	return $notification_topics;
2644
}
2645
2646
/**
2647
 * Gets information about all the boards a user has requested notifications for. Callback for the list in alert_notifications_boards
2648
 *
2649
 * @param int $start Which item to start with (not used here)
2650
 * @param int $items_per_page How many items to show on each page (not used here)
2651
 * @param string $sort A string indicating how to sort the results
2652
 * @param int $memID The ID of the member
2653
 * @return array An array of information about all the boards a user is subscribed to
2654
 */
2655
function list_getBoardNotifications($start, $items_per_page, $sort, $memID)
2656
{
2657
	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...
2658
2659
	require_once($sourcedir . '/Subs-Notify.php');
2660
	$prefs = getNotifyPrefs($memID);
2661
	$prefs = isset($prefs[$memID]) ? $prefs[$memID] : array();
2662
2663
	$request = $smcFunc['db_query']('', '
2664
		SELECT b.id_board, b.name, COALESCE(lb.id_msg, 0) AS board_read, b.id_msg_updated
2665
		FROM {db_prefix}log_notify AS ln
2666
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
2667
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
2668
		WHERE ln.id_member = {int:selected_member}
2669
			AND {query_see_board}
2670
		ORDER BY {raw:sort}',
2671
		array(
2672
			'current_member' => $user_info['id'],
2673
			'selected_member' => $memID,
2674
			'sort' => $sort,
2675
		)
2676
	);
2677
	$notification_boards = array();
2678
	while ($row = $smcFunc['db_fetch_assoc']($request))
2679
		$notification_boards[] = array(
2680
			'id' => $row['id_board'],
2681
			'name' => $row['name'],
2682
			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
2683
			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>',
2684
			'new' => $row['board_read'] < $row['id_msg_updated'],
2685
			'notify_pref' => isset($prefs['board_notify_' . $row['id_board']]) ? $prefs['board_notify_' . $row['id_board']] : (!empty($prefs['board_notify']) ? $prefs['board_notify'] : 0),
2686
		);
2687
	$smcFunc['db_free_result']($request);
2688
2689
	return $notification_boards;
2690
}
2691
2692
/**
2693
 * Loads the theme options for a user
2694
 *
2695
 * @param int $memID The ID of the member
2696
 */
2697
function loadThemeOptions($memID)
2698
{
2699
	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...
2700
2701 View Code Duplication
	if (isset($_POST['default_options']))
2702
		$_POST['options'] = isset($_POST['options']) ? $_POST['options'] + $_POST['default_options'] : $_POST['default_options'];
2703
2704
	if ($context['user']['is_owner'])
2705
	{
2706
		$context['member']['options'] = $options;
2707
		if (isset($_POST['options']) && is_array($_POST['options']))
2708
			foreach ($_POST['options'] as $k => $v)
2709
				$context['member']['options'][$k] = $v;
2710
	}
2711
	else
2712
	{
2713
		$request = $smcFunc['db_query']('', '
2714
			SELECT id_member, variable, value
2715
			FROM {db_prefix}themes
2716
			WHERE id_theme IN (1, {int:member_theme})
2717
				AND id_member IN (-1, {int:selected_member})',
2718
			array(
2719
				'member_theme' => (int) $cur_profile['id_theme'],
2720
				'selected_member' => $memID,
2721
			)
2722
		);
2723
		$temp = array();
2724
		while ($row = $smcFunc['db_fetch_assoc']($request))
2725
		{
2726
			if ($row['id_member'] == -1)
2727
			{
2728
				$temp[$row['variable']] = $row['value'];
2729
				continue;
2730
			}
2731
2732
			if (isset($_POST['options'][$row['variable']]))
2733
				$row['value'] = $_POST['options'][$row['variable']];
2734
			$context['member']['options'][$row['variable']] = $row['value'];
2735
		}
2736
		$smcFunc['db_free_result']($request);
2737
2738
		// Load up the default theme options for any missing.
2739
		foreach ($temp as $k => $v)
2740
		{
2741
			if (!isset($context['member']['options'][$k]))
2742
				$context['member']['options'][$k] = $v;
2743
		}
2744
	}
2745
}
2746
2747
/**
2748
 * Handles the "ignored boards" section of the profile (if enabled)
2749
 *
2750
 * @param int $memID The ID of the member
2751
 */
2752
function ignoreboards($memID)
2753
{
2754
	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...
2755
2756
	// Have the admins enabled this option?
2757
	if (empty($modSettings['allow_ignore_boards']))
2758
		fatal_lang_error('ignoreboards_disallowed', 'user');
2759
2760
	// Find all the boards this user is allowed to see.
2761
	$request = $smcFunc['db_query']('order_by_board_order', '
2762
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level,
2763
			'. (!empty($cur_profile['ignore_boards']) ? 'b.id_board IN ({array_int:ignore_boards})' : '0') . ' AS is_ignored
2764
		FROM {db_prefix}boards AS b
2765
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2766
		WHERE {query_see_board}
2767
			AND redirect = {string:empty_string}',
2768
		array(
2769
			'ignore_boards' => !empty($cur_profile['ignore_boards']) ? explode(',', $cur_profile['ignore_boards']) : array(),
2770
			'empty_string' => '',
2771
		)
2772
	);
2773
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
2774
	$context['categories'] = array();
2775 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2776
	{
2777
		// This category hasn't been set up yet..
2778
		if (!isset($context['categories'][$row['id_cat']]))
2779
			$context['categories'][$row['id_cat']] = array(
2780
				'id' => $row['id_cat'],
2781
				'name' => $row['cat_name'],
2782
				'boards' => array()
2783
			);
2784
2785
		// Set this board up, and let the template know when it's a child.  (indent them..)
2786
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
2787
			'id' => $row['id_board'],
2788
			'name' => $row['name'],
2789
			'child_level' => $row['child_level'],
2790
			'selected' => $row['is_ignored'],
2791
		);
2792
	}
2793
	$smcFunc['db_free_result']($request);
2794
2795
	require_once($sourcedir . '/Subs-Boards.php');
2796
	sortCategories($context['categories']);
2797
2798
	// Now, let's sort the list of categories into the boards for templates that like that.
2799
	$temp_boards = array();
2800 View Code Duplication
	foreach ($context['categories'] as $category)
2801
	{
2802
		// Include a list of boards per category for easy toggling.
2803
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
2804
2805
		$temp_boards[] = array(
2806
			'name' => $category['name'],
2807
			'child_ids' => array_keys($category['boards'])
2808
		);
2809
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
2810
	}
2811
2812
	$max_boards = ceil(count($temp_boards) / 2);
2813
	if ($max_boards == 1)
2814
		$max_boards = 2;
2815
2816
	// Now, alternate them so they can be shown left and right ;).
2817
	$context['board_columns'] = array();
2818 View Code Duplication
	for ($i = 0; $i < $max_boards; $i++)
2819
	{
2820
		$context['board_columns'][] = $temp_boards[$i];
2821
		if (isset($temp_boards[$i + $max_boards]))
2822
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
2823
		else
2824
			$context['board_columns'][] = array();
2825
	}
2826
2827
	loadThemeOptions($memID);
2828
}
2829
2830
/**
2831
 * Load all the languages for the profile
2832
 * .
2833
 * @return bool Whether or not the forum has multiple languages installed
2834
 */
2835
function profileLoadLanguages()
2836
{
2837
	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...
2838
2839
	$context['profile_languages'] = array();
2840
2841
	// Get our languages!
2842
	getLanguages();
2843
2844
	// Setup our languages.
2845
	foreach ($context['languages'] as $lang)
2846
	{
2847
		$context['profile_languages'][$lang['filename']] = strtr($lang['name'], array('-utf8' => ''));
2848
	}
2849
	ksort($context['profile_languages']);
2850
2851
	// Return whether we should proceed with this.
2852
	return count($context['profile_languages']) > 1 ? true : false;
2853
}
2854
2855
/**
2856
 * Handles the "manage groups" section of the profile
2857
 *
2858
 * @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...
2859
 */
2860
function profileLoadGroups()
2861
{
2862
	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...
2863
2864
	$context['member_groups'] = array(
2865
		0 => array(
2866
			'id' => 0,
2867
			'name' => $txt['no_primary_membergroup'],
2868
			'is_primary' => $cur_profile['id_group'] == 0,
2869
			'can_be_additional' => false,
2870
			'can_be_primary' => true,
2871
		)
2872
	);
2873
	$curGroups = explode(',', $cur_profile['additional_groups']);
2874
2875
	// Load membergroups, but only those groups the user can assign.
2876
	$request = $smcFunc['db_query']('', '
2877
		SELECT group_name, id_group, hidden
2878
		FROM {db_prefix}membergroups
2879
		WHERE id_group != {int:moderator_group}
2880
			AND min_posts = {int:min_posts}' . (allowedTo('admin_forum') ? '' : '
2881
			AND group_type != {int:is_protected}') . '
2882
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name',
2883
		array(
2884
			'moderator_group' => 3,
2885
			'min_posts' => -1,
2886
			'is_protected' => 1,
2887
			'newbie_group' => 4,
2888
		)
2889
	);
2890
	while ($row = $smcFunc['db_fetch_assoc']($request))
2891
	{
2892
		// We should skip the administrator group if they don't have the admin_forum permission!
2893
		if ($row['id_group'] == 1 && !allowedTo('admin_forum'))
2894
			continue;
2895
2896
		$context['member_groups'][$row['id_group']] = array(
2897
			'id' => $row['id_group'],
2898
			'name' => $row['group_name'],
2899
			'is_primary' => $cur_profile['id_group'] == $row['id_group'],
2900
			'is_additional' => in_array($row['id_group'], $curGroups),
2901
			'can_be_additional' => true,
2902
			'can_be_primary' => $row['hidden'] != 2,
2903
		);
2904
	}
2905
	$smcFunc['db_free_result']($request);
2906
2907
	$context['member']['group_id'] = $user_settings['id_group'];
2908
2909
	return true;
2910
}
2911
2912
/**
2913
 * Load key signature context data.
2914
 *
2915
 * @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...
2916
 */
2917
function profileLoadSignatureData()
2918
{
2919
	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...
2920
2921
	// Signature limits.
2922
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
2923
	$sig_limits = explode(',', $sig_limits);
2924
2925
	$context['signature_enabled'] = isset($sig_limits[0]) ? $sig_limits[0] : 0;
2926
	$context['signature_limits'] = array(
2927
		'max_length' => isset($sig_limits[1]) ? $sig_limits[1] : 0,
2928
		'max_lines' => isset($sig_limits[2]) ? $sig_limits[2] : 0,
2929
		'max_images' => isset($sig_limits[3]) ? $sig_limits[3] : 0,
2930
		'max_smileys' => isset($sig_limits[4]) ? $sig_limits[4] : 0,
2931
		'max_image_width' => isset($sig_limits[5]) ? $sig_limits[5] : 0,
2932
		'max_image_height' => isset($sig_limits[6]) ? $sig_limits[6] : 0,
2933
		'max_font_size' => isset($sig_limits[7]) ? $sig_limits[7] : 0,
2934
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
2935
	);
2936
	// Kept this line in for backwards compatibility!
2937
	$context['max_signature_length'] = $context['signature_limits']['max_length'];
2938
	// Warning message for signature image limits?
2939
	$context['signature_warning'] = '';
2940
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
2941
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
2942
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
2943
		$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']);
2944
2945
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_charset'] == 'UTF-8' || function_exists('iconv'))));
2946
2947
	if (empty($context['do_preview']))
2948
		$context['member']['signature'] = empty($cur_profile['signature']) ? '' : str_replace(array('<br>', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature']);
2949
	else
2950
	{
2951
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
2952
		$validation = profileValidateSignature($signature);
2953
		if (empty($context['post_errors']))
2954
		{
2955
			loadLanguage('Errors');
2956
			$context['post_errors'] = array();
2957
		}
2958
		$context['post_errors'][] = 'signature_not_yet_saved';
2959
		if ($validation !== true && $validation !== false)
2960
			$context['post_errors'][] = $validation;
2961
2962
		censorText($context['member']['signature']);
2963
		$context['member']['current_signature'] = $context['member']['signature'];
2964
		censorText($signature);
2965
		$context['member']['signature_preview'] = parse_bbc($signature, true, 'sig' . $memberContext[$context['id_member']]);
2966
		$context['member']['signature'] = $_POST['signature'];
2967
	}
2968
2969
	// Load the spell checker?
2970
	if ($context['show_spellchecking'])
2971
		loadJavaScriptFile('spellcheck.js', array('defer' => false), 'smf_spellcheck');
2972
2973
	return true;
2974
}
2975
2976
/**
2977
 * Load avatar context data.
2978
 *
2979
 * @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...
2980
 */
2981
function profileLoadAvatarData()
2982
{
2983
	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...
2984
2985
	$context['avatar_url'] = $modSettings['avatar_url'];
2986
2987
	// Default context.
2988
	$context['member']['avatar'] += array(
2989
		'custom' => stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://') ? $cur_profile['avatar'] : 'http://',
2990
		'selection' => $cur_profile['avatar'] == '' || (stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) ? '' : $cur_profile['avatar'],
2991
		'allow_server_stored' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_server_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2992
		'allow_upload' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_upload_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2993
		'allow_external' => (empty($modSettings['gravatarEnabled']) || empty($modSettings['gravatarOverride'])) && (allowedTo('profile_remote_avatar') || (!$context['user']['is_owner'] && allowedTo('profile_extra_any'))),
2994
		'allow_gravatar' => !empty($modSettings['gravatarEnabled']) || !empty($modSettings['gravatarOverride']),
2995
	);
2996
2997
	if ($context['member']['avatar']['allow_gravatar'] && (stristr($cur_profile['avatar'], 'gravatar://') || !empty($modSettings['gravatarOverride'])))
2998
	{
2999
		$context['member']['avatar'] += array(
3000
			'choice' => 'gravatar',
3001
			'server_pic' => 'blank.png',
3002
			'external' => $cur_profile['avatar'] == 'gravatar://' || empty($modSettings['gravatarAllowExtraEmail']) || !empty($modSettings['gravatarOverride']) ? $cur_profile['email_address'] : substr($cur_profile['avatar'], 11)
3003
		);
3004
		$context['member']['avatar']['href'] = get_gravatar_url($context['member']['avatar']['external']);
3005
	}
3006
	elseif ($cur_profile['avatar'] == '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
3007
	{
3008
		$context['member']['avatar'] += array(
3009
			'choice' => 'upload',
3010
			'server_pic' => 'blank.png',
3011
			'external' => 'http://'
3012
		);
3013
		$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'];
3014
	}
3015
	// Use "avatar_original" here so we show what the user entered even if the image proxy is enabled
3016
	elseif ((stristr($cur_profile['avatar'], 'http://') || stristr($cur_profile['avatar'], 'https://')) && $context['member']['avatar']['allow_external'])
3017
		$context['member']['avatar'] += array(
3018
			'choice' => 'external',
3019
			'server_pic' => 'blank.png',
3020
			'external' => $cur_profile['avatar_original']
3021
		);
3022
	elseif ($cur_profile['avatar'] != '' && file_exists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']) && $context['member']['avatar']['allow_server_stored'])
3023
		$context['member']['avatar'] += array(
3024
			'choice' => 'server_stored',
3025
			'server_pic' => $cur_profile['avatar'] == '' ? 'blank.png' : $cur_profile['avatar'],
3026
			'external' => 'http://'
3027
		);
3028
	else
3029
		$context['member']['avatar'] += array(
3030
			'choice' => 'none',
3031
			'server_pic' => 'blank.png',
3032
			'external' => 'http://'
3033
		);
3034
3035
	// Get a list of all the avatars.
3036
	if ($context['member']['avatar']['allow_server_stored'])
3037
	{
3038
		$context['avatar_list'] = array();
3039
		$context['avatars'] = is_dir($modSettings['avatar_directory']) ? getAvatars('', 0) : array();
3040
	}
3041
	else
3042
		$context['avatars'] = array();
3043
3044
	// Second level selected avatar...
3045
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
3046
	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']);
3047
}
3048
3049
/**
3050
 * Save a members group.
3051
 *
3052
 * @param int &$value The ID of the (new) primary group
3053
 * @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...
3054
 */
3055
function profileSaveGroups(&$value)
3056
{
3057
	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...
3058
3059
	// Do we need to protect some groups?
3060 View Code Duplication
	if (!allowedTo('admin_forum'))
3061
	{
3062
		$request = $smcFunc['db_query']('', '
3063
			SELECT id_group
3064
			FROM {db_prefix}membergroups
3065
			WHERE group_type = {int:is_protected}',
3066
			array(
3067
				'is_protected' => 1,
3068
			)
3069
		);
3070
		$protected_groups = array(1);
3071
		while ($row = $smcFunc['db_fetch_assoc']($request))
3072
			$protected_groups[] = $row['id_group'];
3073
		$smcFunc['db_free_result']($request);
3074
3075
		$protected_groups = array_unique($protected_groups);
3076
	}
3077
3078
	// The account page allows the change of your id_group - but not to a protected group!
3079
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) == 0)
3080
		$value = (int) $value;
3081
	// ... otherwise it's the old group sir.
3082
	else
3083
		$value = $old_profile['id_group'];
3084
3085
	// Find the additional membergroups (if any)
3086
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
3087
	{
3088
		$additional_groups = array();
3089
		foreach ($_POST['additional_groups'] as $group_id)
3090
		{
3091
			$group_id = (int) $group_id;
3092
			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...
3093
				$additional_groups[] = $group_id;
3094
		}
3095
3096
		// Put the protected groups back in there if you don't have permission to take them away.
3097
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
3098
		foreach ($old_additional_groups as $group_id)
3099
		{
3100
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
3101
				$additional_groups[] = $group_id;
3102
		}
3103
3104
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
3105
		{
3106
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
3107
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
3108
		}
3109
	}
3110
3111
	// Too often, people remove delete their own account, or something.
3112
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
3113
	{
3114
		$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...
3115
3116
		// If they would no longer be an admin, look for any other...
3117
		if (!$stillAdmin)
3118
		{
3119
			$request = $smcFunc['db_query']('', '
3120
				SELECT id_member
3121
				FROM {db_prefix}members
3122
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
3123
					AND id_member != {int:selected_member}
3124
				LIMIT 1',
3125
				array(
3126
					'admin_group' => 1,
3127
					'selected_member' => $context['id_member'],
3128
				)
3129
			);
3130
			list ($another) = $smcFunc['db_fetch_row']($request);
3131
			$smcFunc['db_free_result']($request);
3132
3133
			if (empty($another))
3134
				fatal_lang_error('at_least_one_admin', 'critical');
3135
		}
3136
	}
3137
3138
	// If we are changing group status, update permission cache as necessary.
3139
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
3140
	{
3141 View Code Duplication
		if ($context['user']['is_owner'])
3142
			$_SESSION['mc']['time'] = 0;
3143
		else
3144
			updateSettings(array('settings_updated' => time()));
3145
	}
3146
3147
	// Announce to any hooks that we have changed groups, but don't allow them to change it.
3148
	call_integration_hook('integrate_profile_profileSaveGroups', array($value, $additional_groups));
3149
3150
	return true;
3151
}
3152
3153
/**
3154
 * The avatar is incredibly complicated, what with the options... and what not.
3155
 * @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...
3156
 *
3157
 * @param string &$value What kind of avatar we're expecting. Can be 'none', 'server_stored', 'gravatar', 'external' or 'upload'
3158
 * @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...
3159
 */
3160
function profileSaveAvatarData(&$value)
3161
{
3162
	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...
3163
3164
	$memID = $context['id_member'];
3165
	if (empty($memID) && !empty($context['password_auth_failed']))
3166
		return false;
3167
3168
	require_once($sourcedir . '/ManageAttachments.php');
3169
3170
	// We're going to put this on a nice custom dir.
3171
	$uploadDir = $modSettings['custom_avatar_dir'];
3172
	$id_folder = 1;
3173
3174
	$downloadedExternalAvatar = false;
3175
	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']))
3176
	{
3177
		if (!is_writable($uploadDir))
3178
			fatal_lang_error('attachments_no_write', 'critical');
3179
3180
		require_once($sourcedir . '/Subs-Package.php');
3181
3182
		$url = parse_url($_POST['userpicpersonal']);
3183
		$contents = fetch_web_data($url['scheme'] . '://' . $url['host'] . (empty($url['port']) ? '' : ':' . $url['port']) . str_replace(' ', '%20', trim($url['path'])));
3184
3185
		$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...
3186
		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...
3187
		{
3188
			fwrite($tmpAvatar, $contents);
3189
			fclose($tmpAvatar);
3190
3191
			$downloadedExternalAvatar = true;
3192
			$_FILES['attachment']['tmp_name'] = $new_filename;
3193
		}
3194
	}
3195
3196
	// Removes whatever attachment there was before updating
3197
	if ($value == 'none')
3198
	{
3199
		$profile_vars['avatar'] = '';
3200
3201
		// Reset the attach ID.
3202
		$cur_profile['id_attach'] = 0;
3203
		$cur_profile['attachment_type'] = 0;
3204
		$cur_profile['filename'] = '';
3205
3206
		removeAttachments(array('id_member' => $memID));
3207
	}
3208
3209
	// An avatar from the server-stored galleries.
3210
	elseif ($value == 'server_stored' && allowedTo('profile_server_avatar'))
3211
	{
3212
		$profile_vars['avatar'] = strtr(empty($_POST['file']) ? (empty($_POST['cat']) ? '' : $_POST['cat']) : $_POST['file'], array('&amp;' => '&'));
3213
		$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']) : '';
3214
3215
		// Clear current profile...
3216
		$cur_profile['id_attach'] = 0;
3217
		$cur_profile['attachment_type'] = 0;
3218
		$cur_profile['filename'] = '';
3219
3220
		// Get rid of their old avatar. (if uploaded.)
3221
		removeAttachments(array('id_member' => $memID));
3222
	}
3223
	elseif ($value == 'gravatar' && !empty($modSettings['gravatarEnabled']))
3224
	{
3225
		// 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.
3226
		if (empty($_POST['gravatarEmail']) || empty($modSettings['gravatarAllowExtraEmail']) || !filter_var($_POST['gravatarEmail'], FILTER_VALIDATE_EMAIL))
3227
			$profile_vars['avatar'] = 'gravatar://';
3228
		else
3229
			$profile_vars['avatar'] = 'gravatar://' . ($_POST['gravatarEmail'] != $cur_profile['email_address'] ? $_POST['gravatarEmail'] : '');
3230
3231
		// Get rid of their old avatar. (if uploaded.)
3232
		removeAttachments(array('id_member' => $memID));
3233
	}
3234
	elseif ($value == 'external' && allowedTo('profile_remote_avatar') && (stripos($_POST['userpicpersonal'], 'http://') === 0 || stripos($_POST['userpicpersonal'], 'https://') === 0) && empty($modSettings['avatar_download_external']))
3235
	{
3236
		// We need these clean...
3237
		$cur_profile['id_attach'] = 0;
3238
		$cur_profile['attachment_type'] = 0;
3239
		$cur_profile['filename'] = '';
3240
3241
		// Remove any attached avatar...
3242
		removeAttachments(array('id_member' => $memID));
3243
3244
		$profile_vars['avatar'] = str_replace(' ', '%20', preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $_POST['userpicpersonal']));
3245
3246
		if ($profile_vars['avatar'] == 'http://' || $profile_vars['avatar'] == 'http:///')
3247
			$profile_vars['avatar'] = '';
3248
		// Trying to make us do something we'll regret?
3249
		elseif (substr($profile_vars['avatar'], 0, 7) != 'http://' && substr($profile_vars['avatar'], 0, 8) != 'https://')
3250
			return 'bad_avatar_invalid_url';
3251
		// Should we check dimensions?
3252
		elseif (!empty($modSettings['avatar_max_height_external']) || !empty($modSettings['avatar_max_width_external']))
3253
		{
3254
			// Now let's validate the avatar.
3255
			$sizes = url_image_size($profile_vars['avatar']);
3256
3257
			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']))))
3258
			{
3259
				// Houston, we have a problem. The avatar is too large!!
3260
				if ($modSettings['avatar_action_too_large'] == 'option_refuse')
3261
					return 'bad_avatar_too_large';
3262
				elseif ($modSettings['avatar_action_too_large'] == 'option_download_and_resize')
3263
				{
3264
					// @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...
3265
					require_once($sourcedir . '/Subs-Graphics.php');
3266
					if (downloadAvatar($profile_vars['avatar'], $memID, $modSettings['avatar_max_width_external'], $modSettings['avatar_max_height_external']))
3267
					{
3268
						$profile_vars['avatar'] = '';
3269
						$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3270
						$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3271
						$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3272
					}
3273
					else
3274
						return 'bad_avatar';
3275
				}
3276
			}
3277
		}
3278
	}
3279
	elseif (($value == 'upload' && allowedTo('profile_upload_avatar')) || $downloadedExternalAvatar)
3280
	{
3281
		if ((isset($_FILES['attachment']['name']) && $_FILES['attachment']['name'] != '') || $downloadedExternalAvatar)
3282
		{
3283
			// Get the dimensions of the image.
3284
			if (!$downloadedExternalAvatar)
3285
			{
3286
				if (!is_writable($uploadDir))
3287
					fatal_lang_error('attachments_no_write', 'critical');
3288
3289
				$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...
3290
				if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $new_filename))
3291
					fatal_lang_error('attach_timeout', 'critical');
3292
3293
				$_FILES['attachment']['tmp_name'] = $new_filename;
3294
			}
3295
3296
			$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3297
3298
			// No size, then it's probably not a valid pic.
3299
			if ($sizes === false)
3300
			{
3301
				@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...
3302
				return 'bad_avatar';
3303
			}
3304
			// Check whether the image is too large.
3305
			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']))
3306
			{
3307
				if (!empty($modSettings['avatar_resize_upload']))
3308
				{
3309
					// Attempt to chmod it.
3310
					smf_chmod($_FILES['attachment']['tmp_name'], 0644);
3311
3312
					// @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...
3313
					require_once($sourcedir . '/Subs-Graphics.php');
3314
					if (!downloadAvatar($_FILES['attachment']['tmp_name'], $memID, $modSettings['avatar_max_width_upload'], $modSettings['avatar_max_height_upload']))
3315
					{
3316
						@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...
3317
						return 'bad_avatar';
3318
					}
3319
3320
					// Reset attachment avatar data.
3321
					$cur_profile['id_attach'] = $modSettings['new_avatar_data']['id'];
3322
					$cur_profile['filename'] = $modSettings['new_avatar_data']['filename'];
3323
					$cur_profile['attachment_type'] = $modSettings['new_avatar_data']['type'];
3324
				}
3325
3326
				// Admin doesn't want to resize large avatars, can't do much about it but to tell you to use a different one :(
3327
				else
3328
				{
3329
					@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...
3330
					return 'bad_avatar_too_large';
3331
				}
3332
			}
3333
3334
			// So far, so good, checks lies ahead!
3335
			elseif (is_array($sizes))
3336
			{
3337
				// Now try to find an infection.
3338
				require_once($sourcedir . '/Subs-Graphics.php');
3339
				if (!checkImageContents($_FILES['attachment']['tmp_name'], !empty($modSettings['avatar_paranoid'])))
3340
				{
3341
					// It's bad. Try to re-encode the contents?
3342
					if (empty($modSettings['avatar_reencode']) || (!reencodeImage($_FILES['attachment']['tmp_name'], $sizes[2])))
3343
					{
3344
						@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...
3345
						return 'bad_avatar_fail_reencode';
3346
					}
3347
					// We were successful. However, at what price?
3348
					$sizes = @getimagesize($_FILES['attachment']['tmp_name']);
3349
					// Hard to believe this would happen, but can you bet?
3350
					if ($sizes === false)
3351
					{
3352
						@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...
3353
						return 'bad_avatar';
3354
					}
3355
				}
3356
3357
				$extensions = array(
3358
					'1' => 'gif',
3359
					'2' => 'jpg',
3360
					'3' => 'png',
3361
					'6' => 'bmp'
3362
				);
3363
3364
				$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
3365
				$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
3366
				$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
3367
				list ($width, $height) = getimagesize($_FILES['attachment']['tmp_name']);
3368
				$file_hash = '';
3369
3370
				// Remove previous attachments this member might have had.
3371
				removeAttachments(array('id_member' => $memID));
3372
3373
				$cur_profile['id_attach'] = $smcFunc['db_insert']('',
3374
					'{db_prefix}attachments',
3375
					array(
3376
						'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
3377
						'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
3378
					),
3379
					array(
3380
						$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
3381
						(int) $width, (int) $height, $mime_type, $id_folder,
3382
					),
3383
					array('id_attach'),
3384
					1
3385
				);
3386
3387
				$cur_profile['filename'] = $destName;
3388
				$cur_profile['attachment_type'] = 1;
3389
3390
				$destinationPath = $uploadDir . '/' . (empty($file_hash) ? $destName : $cur_profile['id_attach'] . '_' . $file_hash . '.dat');
3391
				if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
3392
				{
3393
					// I guess a man can try.
3394
					removeAttachments(array('id_member' => $memID));
3395
					fatal_lang_error('attach_timeout', 'critical');
3396
				}
3397
3398
				// Attempt to chmod it.
3399
				smf_chmod($uploadDir . '/' . $destinationPath, 0644);
3400
			}
3401
			$profile_vars['avatar'] = '';
3402
3403
			// Delete any temporary file.
3404
			if (file_exists($_FILES['attachment']['tmp_name']))
3405
				@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...
3406
		}
3407
		// Selected the upload avatar option and had one already uploaded before or didn't upload one.
3408
		else
3409
			$profile_vars['avatar'] = '';
3410
	}
3411
	elseif ($value == 'gravatar' && allowedTo('profile_gravatar_avatar'))
3412
		$profile_vars['avatar'] = 'gravatar://www.gravatar.com/avatar/' . md5(strtolower(trim($cur_profile['email_address'])));
3413
	else
3414
		$profile_vars['avatar'] = '';
3415
3416
	// Setup the profile variables so it shows things right on display!
3417
	$cur_profile['avatar'] = $profile_vars['avatar'];
3418
3419
	return false;
3420
}
3421
3422
/**
3423
 * Validate the signature
3424
 *
3425
 * @param string &$value The new signature
3426
 * @return bool|string True if the signature passes the checks, otherwise a string indicating what the problem is
3427
 */
3428
function profileValidateSignature(&$value)
3429
{
3430
	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...
3431
3432
	require_once($sourcedir . '/Subs-Post.php');
3433
3434
	// Admins can do whatever they hell they want!
3435
	if (!allowedTo('admin_forum'))
3436
	{
3437
		// Load all the signature limits.
3438
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
3439
		$sig_limits = explode(',', $sig_limits);
3440
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
3441
3442
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
3443
3444
		// Too many lines?
3445
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
3446
		{
3447
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
3448
			return 'signature_max_lines';
3449
		}
3450
3451
		// Too many images?!
3452 View Code Duplication
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
3453
		{
3454
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
3455
			return 'signature_max_image_count';
3456
		}
3457
3458
		// What about too many smileys!
3459
		$smiley_parsed = $unparsed_signature;
3460
		parsesmileys($smiley_parsed);
3461
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
3462
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
3463
			return 'signature_allow_smileys';
3464
		elseif (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
3465
		{
3466
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
3467
			return 'signature_max_smileys';
3468
		}
3469
3470
		// Maybe we are abusing font sizes?
3471
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)?(px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false && isset($matches[2]))
3472
		{
3473
			foreach ($matches[1] as $ind => $size)
3474
			{
3475
				$limit_broke = 0;
3476
				// Attempt to allow all sizes of abuse, so to speak.
3477 View Code Duplication
				if ($matches[2][$ind] == 'px' && $size > $sig_limits[7])
3478
					$limit_broke = $sig_limits[7] . 'px';
3479
				elseif ($matches[2][$ind] == 'pt' && $size > ($sig_limits[7] * 0.75))
3480
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
3481
				elseif ($matches[2][$ind] == 'em' && $size > ((float) $sig_limits[7] / 16))
3482
					$limit_broke = ((float) $sig_limits[7] / 16) . 'em';
3483
				elseif ($matches[2][$ind] != 'px' && $matches[2][$ind] != 'pt' && $matches[2][$ind] != 'em' && $sig_limits[7] < 18)
3484
					$limit_broke = 'large';
3485
3486
				if ($limit_broke)
3487
				{
3488
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
3489
					return 'signature_max_font_size';
3490
				}
3491
			}
3492
		}
3493
3494
		// The difficult one - image sizes! Don't error on this - just fix it.
3495
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
3496
		{
3497
			// Get all BBC tags...
3498
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br>)*([^<">]+?)(?:<br>)*\[/img\]~i', $unparsed_signature, $matches);
3499
			// ... and all HTML ones.
3500
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?' . '>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
3501
			// And stick the HTML in the BBC.
3502 View Code Duplication
			if (!empty($matches2))
3503
			{
3504
				foreach ($matches2[0] as $ind => $dummy)
3505
				{
3506
					$matches[0][] = $matches2[0][$ind];
3507
					$matches[1][] = '';
3508
					$matches[2][] = '';
3509
					$matches[3][] = '';
3510
					$matches[4][] = '';
3511
					$matches[5][] = '';
3512
					$matches[6][] = '';
3513
					$matches[7][] = $matches2[1][$ind];
3514
				}
3515
			}
3516
3517
			$replaces = array();
3518
			// Try to find all the images!
3519
			if (!empty($matches))
3520
			{
3521
				foreach ($matches[0] as $key => $image)
3522
				{
3523
					$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...
3524
3525
					// Does it have predefined restraints? Width first.
3526 View Code Duplication
					if ($matches[6][$key])
3527
						$matches[2][$key] = $matches[6][$key];
3528 View Code Duplication
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
3529
					{
3530
						$width = $sig_limits[5];
3531
						$matches[4][$key] = $matches[4][$key] * ($width / $matches[2][$key]);
3532
					}
3533
					elseif ($matches[2][$key])
3534
						$width = $matches[2][$key];
3535
					// ... and height.
3536 View Code Duplication
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
3537
					{
3538
						$height = $sig_limits[6];
3539
						if ($width != -1)
3540
							$width = $width * ($height / $matches[4][$key]);
3541
					}
3542
					elseif ($matches[4][$key])
3543
						$height = $matches[4][$key];
3544
3545
					// If the dimensions are still not fixed - we need to check the actual image.
3546 View Code Duplication
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
3547
					{
3548
						$sizes = url_image_size($matches[7][$key]);
3549
						if (is_array($sizes))
3550
						{
3551
							// Too wide?
3552
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
3553
							{
3554
								$width = $sig_limits[5];
3555
								$sizes[1] = $sizes[1] * ($width / $sizes[0]);
3556
							}
3557
							// Too high?
3558
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
3559
							{
3560
								$height = $sig_limits[6];
3561
								if ($width == -1)
3562
									$width = $sizes[0];
3563
								$width = $width * ($height / $sizes[1]);
3564
							}
3565
							elseif ($width != -1)
3566
								$height = $sizes[1];
3567
						}
3568
					}
3569
3570
					// Did we come up with some changes? If so remake the string.
3571 View Code Duplication
					if ($width != -1 || $height != -1)
3572
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
3573
				}
3574
				if (!empty($replaces))
3575
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
3576
			}
3577
		}
3578
3579
		// Any disabled BBC?
3580
		$disabledSigBBC = implode('|', $disabledTags);
3581
		if (!empty($disabledSigBBC))
3582
		{
3583
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
3584
			{
3585
				$disabledTags = array_unique($disabledTags);
3586
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
3587
				return 'signature_disabled_bbc';
3588
			}
3589
		}
3590
	}
3591
3592
	preparsecode($value);
3593
3594
	// Too long?
3595
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && $smcFunc['strlen'](str_replace('<br>', "\n", $value)) > $sig_limits[1])
3596
	{
3597
		$_POST['signature'] = trim($smcFunc['htmlspecialchars'](str_replace('<br>', "\n", $value), ENT_QUOTES));
3598
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
3599
		return 'signature_max_length';
3600
	}
3601
3602
	return true;
3603
}
3604
3605
/**
3606
 * Validate an email address.
3607
 *
3608
 * @param string $email The email address to validate
3609
 * @param int $memID The ID of the member (used to prevent false positives from the current user)
3610
 * @return bool|string True if the email is valid, otherwise a string indicating what the problem is
3611
 */
3612
function profileValidateEmail($email, $memID = 0)
3613
{
3614
	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...
3615
3616
	$email = strtr($email, array('&#039;' => '\''));
3617
3618
	// Check the name and email for validity.
3619
	if (trim($email) == '')
3620
		return 'no_email';
3621
	if (!filter_var($email, FILTER_VALIDATE_EMAIL))
3622
		return 'bad_email';
3623
3624
	// Email addresses should be and stay unique.
3625
	$request = $smcFunc['db_query']('', '
3626
		SELECT id_member
3627
		FROM {db_prefix}members
3628
		WHERE ' . ($memID != 0 ? 'id_member != {int:selected_member} AND ' : '') . '
3629
			email_address = {string:email_address}
3630
		LIMIT 1',
3631
		array(
3632
			'selected_member' => $memID,
3633
			'email_address' => $email,
3634
		)
3635
	);
3636
3637
	if ($smcFunc['db_num_rows']($request) > 0)
3638
		return 'email_taken';
3639
	$smcFunc['db_free_result']($request);
3640
3641
	return true;
3642
}
3643
3644
/**
3645
 * Reload a user's settings.
3646
 */
3647
function profileReloadUser()
3648
{
3649
	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...
3650
3651
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] != '')
3652
		setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash_salt($_POST['passwrd1'], $cur_profile['password_salt']));
3653
3654
	loadUserSettings();
3655
	writeLog();
3656
}
3657
3658
/**
3659
 * Send the user a new activation email if they need to reactivate!
3660
 */
3661
function profileSendActivation()
3662
{
3663
	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...
3664
3665
	require_once($sourcedir . '/Subs-Post.php');
3666
3667
	// Shouldn't happen but just in case.
3668
	if (empty($profile_vars['email_address']))
3669
		return;
3670
3671
	$replacements = array(
3672
		'ACTIVATIONLINK' => $scripturl . '?action=activate;u=' . $context['id_member'] . ';code=' . $profile_vars['validation_code'],
3673
		'ACTIVATIONCODE' => $profile_vars['validation_code'],
3674
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=activate;u=' . $context['id_member'],
3675
	);
3676
3677
	// Send off the email.
3678
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
3679
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, 'reactivate', $emaildata['is_html'], 0);
3680
3681
	// Log the user out.
3682
	$smcFunc['db_query']('', '
3683
		DELETE FROM {db_prefix}log_online
3684
		WHERE id_member = {int:selected_member}',
3685
		array(
3686
			'selected_member' => $context['id_member'],
3687
		)
3688
	);
3689
	$_SESSION['log_time'] = 0;
3690
	$_SESSION['login_' . $cookiename] = $smcFunc['json_encode'](array(0, '', 0));
3691
3692
	if (isset($_COOKIE[$cookiename]))
3693
		$_COOKIE[$cookiename] = '';
3694
3695
	loadUserSettings();
3696
3697
	$context['user']['is_logged'] = false;
3698
	$context['user']['is_guest'] = true;
3699
3700
	redirectexit('action=sendactivation');
3701
}
3702
3703
/**
3704
 * Function to allow the user to choose group membership etc...
3705
 *
3706
 * @param int $memID The ID of the member
3707
 */
3708
function groupMembership($memID)
3709
{
3710
	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...
3711
3712
	$curMember = $user_profile[$memID];
3713
	$context['primary_group'] = $curMember['id_group'];
3714
3715
	// Can they manage groups?
3716
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3717
	$context['can_manage_protected'] = allowedTo('admin_forum');
3718
	$context['can_edit_primary'] = $context['can_manage_protected'];
3719
	$context['update_message'] = isset($_GET['msg']) && isset($txt['group_membership_msg_' . $_GET['msg']]) ? $txt['group_membership_msg_' . $_GET['msg']] : '';
3720
3721
	// Get all the groups this user is a member of.
3722
	$groups = explode(',', $curMember['additional_groups']);
3723
	$groups[] = $curMember['id_group'];
3724
3725
	// Ensure the query doesn't croak!
3726
	if (empty($groups))
3727
		$groups = array(0);
3728
	// Just to be sure...
3729
	foreach ($groups as $k => $v)
3730
		$groups[$k] = (int) $v;
3731
3732
	// Get all the membergroups they can join.
3733
	$request = $smcFunc['db_query']('', '
3734
		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
3735
			COALESCE(lgr.id_member, 0) AS pending
3736
		FROM {db_prefix}membergroups AS mg
3737
			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})
3738
		WHERE (mg.id_group IN ({array_int:group_list})
3739
			OR mg.group_type > {int:nonjoin_group_id})
3740
			AND mg.min_posts = {int:min_posts}
3741
			AND mg.id_group != {int:moderator_group}
3742
		ORDER BY group_name',
3743
		array(
3744
			'group_list' => $groups,
3745
			'selected_member' => $memID,
3746
			'status_open' => 0,
3747
			'nonjoin_group_id' => 1,
3748
			'min_posts' => -1,
3749
			'moderator_group' => 3,
3750
		)
3751
	);
3752
	// This beast will be our group holder.
3753
	$context['groups'] = array(
3754
		'member' => array(),
3755
		'available' => array()
3756
	);
3757
	while ($row = $smcFunc['db_fetch_assoc']($request))
3758
	{
3759
		// Can they edit their primary group?
3760
		if (($row['id_group'] == $context['primary_group'] && $row['group_type'] > 1) || ($row['hidden'] != 2 && $context['primary_group'] == 0 && in_array($row['id_group'], $groups)))
3761
			$context['can_edit_primary'] = true;
3762
3763
		// If they can't manage (protected) groups, and it's not publically joinable or already assigned, they can't see it.
3764
		if (((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0)) && $row['id_group'] != $context['primary_group'])
3765
			continue;
3766
3767
		$context['groups'][in_array($row['id_group'], $groups) ? 'member' : 'available'][$row['id_group']] = array(
3768
			'id' => $row['id_group'],
3769
			'name' => $row['group_name'],
3770
			'desc' => $row['description'],
3771
			'color' => $row['online_color'],
3772
			'type' => $row['group_type'],
3773
			'pending' => $row['pending'],
3774
			'is_primary' => $row['id_group'] == $context['primary_group'],
3775
			'can_be_primary' => $row['hidden'] != 2,
3776
			// Anything more than this needs to be done through account settings for security.
3777
			'can_leave' => $row['id_group'] != 1 && $row['group_type'] > 1 ? true : false,
3778
		);
3779
	}
3780
	$smcFunc['db_free_result']($request);
3781
3782
	// Add registered members on the end.
3783
	$context['groups']['member'][0] = array(
3784
		'id' => 0,
3785
		'name' => $txt['regular_members'],
3786
		'desc' => $txt['regular_members_desc'],
3787
		'type' => 0,
3788
		'is_primary' => $context['primary_group'] == 0 ? true : false,
3789
		'can_be_primary' => true,
3790
		'can_leave' => 0,
3791
	);
3792
3793
	// No changing primary one unless you have enough groups!
3794
	if (count($context['groups']['member']) < 2)
3795
		$context['can_edit_primary'] = false;
3796
3797
	// In the special case that someone is requesting membership of a group, setup some special context vars.
3798
	if (isset($_REQUEST['request']) && isset($context['groups']['available'][(int) $_REQUEST['request']]) && $context['groups']['available'][(int) $_REQUEST['request']]['type'] == 2)
3799
		$context['group_request'] = $context['groups']['available'][(int) $_REQUEST['request']];
3800
}
3801
3802
/**
3803
 * This function actually makes all the group changes
3804
 *
3805
 * @param array $profile_vars The profile variables
3806
 * @param array $post_errors Any errors that have occurred
3807
 * @param int $memID The ID of the member
3808
 * @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
3809
 */
3810
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...
3811
{
3812
	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...
3813
3814
	// Let's be extra cautious...
3815
	if (!$context['user']['is_owner'] || empty($modSettings['show_group_membership']))
3816
		isAllowedTo('manage_membergroups');
3817
	if (!isset($_REQUEST['gid']) && !isset($_POST['primary']))
3818
		fatal_lang_error('no_access', false);
3819
3820
	checkSession(isset($_GET['gid']) ? 'get' : 'post');
3821
3822
	$old_profile = &$user_profile[$memID];
3823
	$context['can_manage_membergroups'] = allowedTo('manage_membergroups');
3824
	$context['can_manage_protected'] = allowedTo('admin_forum');
3825
3826
	// By default the new primary is the old one.
3827
	$newPrimary = $old_profile['id_group'];
3828
	$addGroups = array_flip(explode(',', $old_profile['additional_groups']));
3829
	$canChangePrimary = $old_profile['id_group'] == 0 ? 1 : 0;
3830
	$changeType = isset($_POST['primary']) ? 'primary' : (isset($_POST['req']) ? 'request' : 'free');
3831
3832
	// One way or another, we have a target group in mind...
3833
	$group_id = isset($_REQUEST['gid']) ? (int) $_REQUEST['gid'] : (int) $_POST['primary'];
3834
	$foundTarget = $changeType == 'primary' && $group_id == 0 ? true : false;
3835
3836
	// Sanity check!!
3837
	if ($group_id == 1)
3838
		isAllowedTo('admin_forum');
3839
	// Protected groups too!
3840
	else
3841
	{
3842
		$request = $smcFunc['db_query']('', '
3843
			SELECT group_type
3844
			FROM {db_prefix}membergroups
3845
			WHERE id_group = {int:current_group}
3846
			LIMIT {int:limit}',
3847
			array(
3848
				'current_group' => $group_id,
3849
				'limit' => 1,
3850
			)
3851
		);
3852
		list ($is_protected) = $smcFunc['db_fetch_row']($request);
3853
		$smcFunc['db_free_result']($request);
3854
3855
		if ($is_protected == 1)
3856
			isAllowedTo('admin_forum');
3857
	}
3858
3859
	// What ever we are doing, we need to determine if changing primary is possible!
3860
	$request = $smcFunc['db_query']('', '
3861
		SELECT id_group, group_type, hidden, group_name
3862
		FROM {db_prefix}membergroups
3863
		WHERE id_group IN ({int:group_list}, {int:current_group})',
3864
		array(
3865
			'group_list' => $group_id,
3866
			'current_group' => $old_profile['id_group'],
3867
		)
3868
	);
3869
	while ($row = $smcFunc['db_fetch_assoc']($request))
3870
	{
3871
		// Is this the new group?
3872
		if ($row['id_group'] == $group_id)
3873
		{
3874
			$foundTarget = true;
3875
			$group_name = $row['group_name'];
3876
3877
			// Does the group type match what we're doing - are we trying to request a non-requestable group?
3878
			if ($changeType == 'request' && $row['group_type'] != 2)
3879
				fatal_lang_error('no_access', false);
3880
			// What about leaving a requestable group we are not a member of?
3881
			elseif ($changeType == 'free' && $row['group_type'] == 2 && $old_profile['id_group'] != $row['id_group'] && !isset($addGroups[$row['id_group']]))
3882
				fatal_lang_error('no_access', false);
3883
			elseif ($changeType == 'free' && $row['group_type'] != 3 && $row['group_type'] != 2)
3884
				fatal_lang_error('no_access', false);
3885
3886
			// We can't change the primary group if this is hidden!
3887
			if ($row['hidden'] == 2)
3888
				$canChangePrimary = false;
3889
		}
3890
3891
		// If this is their old primary, can we change it?
3892 View Code Duplication
		if ($row['id_group'] == $old_profile['id_group'] && ($row['group_type'] > 1 || $context['can_manage_membergroups']) && $canChangePrimary !== false)
3893
			$canChangePrimary = 1;
3894
3895
		// If we are not doing a force primary move, don't do it automatically if current primary is not 0.
3896
		if ($changeType != 'primary' && $old_profile['id_group'] != 0)
3897
			$canChangePrimary = false;
3898
3899
		// If this is the one we are acting on, can we even act?
3900 View Code Duplication
		if ((!$context['can_manage_protected'] && $row['group_type'] == 1) || (!$context['can_manage_membergroups'] && $row['group_type'] == 0))
3901
			$canChangePrimary = false;
3902
	}
3903
	$smcFunc['db_free_result']($request);
3904
3905
	// Didn't find the target?
3906
	if (!$foundTarget)
3907
		fatal_lang_error('no_access', false);
3908
3909
	// Final security check, don't allow users to promote themselves to admin.
3910
	if ($context['can_manage_membergroups'] && !allowedTo('admin_forum'))
3911
	{
3912
		$request = $smcFunc['db_query']('', '
3913
			SELECT COUNT(permission)
3914
			FROM {db_prefix}permissions
3915
			WHERE id_group = {int:selected_group}
3916
				AND permission = {string:admin_forum}
3917
				AND add_deny = {int:not_denied}',
3918
			array(
3919
				'selected_group' => $group_id,
3920
				'not_denied' => 1,
3921
				'admin_forum' => 'admin_forum',
3922
			)
3923
		);
3924
		list ($disallow) = $smcFunc['db_fetch_row']($request);
3925
		$smcFunc['db_free_result']($request);
3926
3927
		if ($disallow)
3928
			isAllowedTo('admin_forum');
3929
	}
3930
3931
	// If we're requesting, add the note then return.
3932
	if ($changeType == 'request')
3933
	{
3934
		$request = $smcFunc['db_query']('', '
3935
			SELECT id_member
3936
			FROM {db_prefix}log_group_requests
3937
			WHERE id_member = {int:selected_member}
3938
				AND id_group = {int:selected_group}
3939
				AND status = {int:status_open}',
3940
			array(
3941
				'selected_member' => $memID,
3942
				'selected_group' => $group_id,
3943
				'status_open' => 0,
3944
			)
3945
		);
3946
		if ($smcFunc['db_num_rows']($request) != 0)
3947
			fatal_lang_error('profile_error_already_requested_group');
3948
		$smcFunc['db_free_result']($request);
3949
3950
		// Log the request.
3951
		$smcFunc['db_insert']('',
3952
			'{db_prefix}log_group_requests',
3953
			array(
3954
				'id_member' => 'int', 'id_group' => 'int', 'time_applied' => 'int', 'reason' => 'string-65534',
3955
				'status' => 'int', 'id_member_acted' => 'int', 'member_name_acted' => 'string', 'time_acted' => 'int', 'act_reason' => 'string',
3956
			),
3957
			array(
3958
				$memID, $group_id, time(), $_POST['reason'],
3959
				0, 0, '', 0, '',
3960
			),
3961
			array('id_request')
3962
		);
3963
3964
		// Set up some data for our background task...
3965
		$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...
3966
3967
		// Add a background task to handle notifying people of this request
3968
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
3969
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
3970
			array('$sourcedir/tasks/GroupReq-Notify.php', 'GroupReq_Notify_Background', $data, 0), array()
3971
		);
3972
3973
		return $changeType;
3974
	}
3975
	// Otherwise we are leaving/joining a group.
3976
	elseif ($changeType == 'free')
3977
	{
3978
		// Are we leaving?
3979
		if ($old_profile['id_group'] == $group_id || isset($addGroups[$group_id]))
3980
		{
3981
			if ($old_profile['id_group'] == $group_id)
3982
				$newPrimary = 0;
3983
			else
3984
				unset($addGroups[$group_id]);
3985
		}
3986
		// ... if not, must be joining.
3987
		else
3988
		{
3989
			// Can we change the primary, and do we want to?
3990
			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...
3991
			{
3992
				if ($old_profile['id_group'] != 0)
3993
					$addGroups[$old_profile['id_group']] = -1;
3994
				$newPrimary = $group_id;
3995
			}
3996
			// Otherwise it's an additional group...
3997
			else
3998
				$addGroups[$group_id] = -1;
3999
		}
4000
	}
4001
	// Finally, we must be setting the primary.
4002
	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...
4003
	{
4004
		if ($old_profile['id_group'] != 0)
4005
			$addGroups[$old_profile['id_group']] = -1;
4006
		if (isset($addGroups[$group_id]))
4007
			unset($addGroups[$group_id]);
4008
		$newPrimary = $group_id;
4009
	}
4010
4011
	// Finally, we can make the changes!
4012
	foreach ($addGroups as $id => $dummy)
4013
		if (empty($id))
4014
			unset($addGroups[$id]);
4015
	$addGroups = implode(',', array_flip($addGroups));
4016
4017
	// Ensure that we don't cache permissions if the group is changing.
4018 View Code Duplication
	if ($context['user']['is_owner'])
4019
		$_SESSION['mc']['time'] = 0;
4020
	else
4021
		updateSettings(array('settings_updated' => time()));
4022
4023
	updateMemberData($memID, array('id_group' => $newPrimary, 'additional_groups' => $addGroups));
4024
4025
	return $changeType;
4026
}
4027
4028
/**
4029
 * Provides interface to setup Two Factor Auth in SMF
4030
 *
4031
 * @param int $memID The ID of the member
4032
 */
4033
function tfasetup($memID)
4034
{
4035
	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...
4036
4037
	require_once($sourcedir . '/Class-TOTP.php');
4038
	require_once($sourcedir . '/Subs-Auth.php');
4039
4040
	// If TFA has not been setup, allow them to set it up
4041
	if (empty($user_settings['tfa_secret']) && $context['user']['is_owner'])
4042
	{
4043
		// Check to ensure we're forcing SSL for authentication
4044 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...
4045
			fatal_lang_error('login_ssl_required');
4046
4047
		// In some cases (forced 2FA or backup code) they would be forced to be redirected here,
4048
		// we do not want too much AJAX to confuse them.
4049
		if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && !isset($_REQUEST['backup']) && !isset($_REQUEST['forced']))
4050
		{
4051
			$context['from_ajax'] = true;
4052
			$context['template_layers'] = array();
4053
		}
4054
4055
		// When the code is being sent, verify to make sure the user got it right
4056
		if (!empty($_REQUEST['save']) && !empty($_SESSION['tfa_secret']))
4057
		{
4058
			$code = $_POST['tfa_code'];
4059
			$totp = new \TOTP\Auth($_SESSION['tfa_secret']);
4060
			$totp->setRange(1);
4061
			$valid_password = hash_verify_password($user_settings['member_name'], trim($_POST['passwd']), $user_settings['passwd']);
4062
			$valid_code = strlen($code) == $totp->getCodeLength() && $totp->validateCode($code);
4063
4064
			if ($valid_password && $valid_code)
4065
			{
4066
				$backup = substr(sha1(mt_rand()), 0, 16);
4067
				$backup_encrypted = hash_password($user_settings['member_name'], $backup);
4068
4069
				updateMemberData($memID, array(
4070
					'tfa_secret' => $_SESSION['tfa_secret'],
4071
					'tfa_backup' => $backup_encrypted,
4072
				));
4073
4074
				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 4067 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...
4075
4076
				unset($_SESSION['tfa_secret']);
4077
4078
				$context['tfa_backup'] = $backup;
4079
				$context['sub_template'] = 'tfasetup_backup';
4080
4081
				return;
4082
			}
4083
			else
4084
			{
4085
				$context['tfa_secret'] = $_SESSION['tfa_secret'];
4086
				$context['tfa_error'] = !$valid_code;
4087
				$context['tfa_pass_error'] = !$valid_password;
4088
				$context['tfa_pass_value'] = $_POST['passwd'];
4089
				$context['tfa_value'] = $_POST['tfa_code'];
4090
			}
4091
		}
4092
		else
4093
		{
4094
			$totp = new \TOTP\Auth();
4095
			$secret = $totp->generateCode();
4096
			$_SESSION['tfa_secret'] = $secret;
4097
			$context['tfa_secret'] = $secret;
4098
			$context['tfa_backup'] = isset($_REQUEST['backup']);
4099
		}
4100
4101
		$context['tfa_qr_url'] = $totp->getQrCodeUrl($context['forum_name'] . ':' . $user_info['name'], $context['tfa_secret']);
4102
	}
4103
	elseif (isset($_REQUEST['disable']))
4104
	{
4105
		updateMemberData($memID, array(
4106
			'tfa_secret' => '',
4107
			'tfa_backup' => '',
4108
		));
4109
		redirectexit('action=profile;area=account;u=' . $memID);
4110
	}
4111
	else
4112
		redirectexit('action=profile;area=account;u=' . $memID);
4113
}
4114
4115
?>