Issues (1686)

sources/subs/Profile.subs.php (20 issues)

1
<?php
2
3
/**
4
 * Functions to support the profile controller
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
use BBC\ParserWrapper;
18
use ElkArte\Cache\Cache;
19
use ElkArte\Controller\Avatars;
20
use ElkArte\Errors\ErrorContext;
21
use ElkArte\Helper\FileFunctions;
22
use ElkArte\Helper\Util;
23
use ElkArte\Languages\Txt;
24
use ElkArte\MembersList;
25
use ElkArte\Notifications\Notifications;
26
use ElkArte\Profile\ProfileFields;
27
use ElkArte\User;
28
29
/**
30
 * Find the ID of the "current" member
31
 *
32
 * @param bool $fatal if the function ends in a fatal error in case of problems (default true)
33
 * @param bool $reload_id if true the already set value is ignored (default false)
34
 *
35
 * @return int if no error.  May return false in case of problems only if $fatal is set to false
36
 * @throws \ElkArte\Exceptions\Exception not_a_user
37
 */
38 12
function currentMemberID($fatal = true, $reload_id = false)
39
{
40
	static $memID;
41 12
42
	// If we already know who we're dealing with
43 10
	if (isset($memID) && !$reload_id)
44
	{
45
		return (int) $memID;
46
	}
47 2
48
	// Did we get the user by name...
49
	if (isset($_REQUEST['user']))
50
	{
51
		$memberResult = MembersList::load($_REQUEST['user'], true, 'profile');
52 2
	}
53
	// ... or by id_member?
54
	elseif (!empty($_REQUEST['u']))
55
	{
56
		$memberResult = MembersList::load((int) $_REQUEST['u'], false, 'profile');
57
	}
58
	// If it was just ?action=profile, edit your own profile.
59 2
	else
60
	{
61
		$memberResult = MembersList::load(User::$info->id, false, 'profile');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
62
	}
63 2
64
	// Check if \ElkArte\MembersList::load() has returned a valid result.
65
	if (!is_array($memberResult))
66
	{
67
		// Members only...
68
		is_not_guest('', $fatal);
69
70
		if ($fatal)
71
		{
72
			throw new \ElkArte\Exceptions\Exception('not_a_user', false);
73
		}
74
75
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
76
	}
77
78
	// If all went well, we have a valid member ID!
79 2
	list ($memID) = $memberResult;
80
81 2
	// Cast here is probably not needed, but don't trust yet !
82
	return (int) $memID;
83
}
84
85
/**
86
 * Set the context for a page load!
87
 *
88
 * @param array $fields
89
 * @param string $hook a string that represent the hook that can be used to operate on $fields
90
 */
91
function setupProfileContext($fields, $hook = '')
92
{
93 2
	global $profile_fields, $context, $cur_profile, $txt;
94
95 2
	if (!empty($hook))
96
	{
97 2
		call_integration_hook('integrate_' . $hook . '_profile_fields', array(&$fields));
98
	}
99
100
	// Make sure we have this!
101 2
	$profileFields = new ProfileFields();
102
	$profileFields->loadProfileFields(true);
103
104 2
	// First check for any linked sets.
105
	foreach ($profile_fields as $key => $field)
106 2
	{
107
		if (isset($field['link_with']) && in_array($field['link_with'], $fields, true))
108 1
		{
109
			$fields[] = $key;
110
		}
111
	}
112
113 2
	// Some default bits.
114 2
	$context['profile_prehtml'] = '';
115 2
	$context['profile_posthtml'] = '';
116
	$context['profile_onsubmit_javascript'] = '';
117 2
118 2
	$i = 0;
119 2
	$last_type = '';
120
	foreach ($fields as $field)
121 2
	{
122
		if (isset($profile_fields[$field]))
123
		{
124 2
			// Shortcut.
125
			$cur_field = &$profile_fields[$field];
126
127 2
			// Does it have a preload and does that preload succeed?
128
			if (isset($cur_field['preload']) && !$cur_field['preload']())
129 2
			{
130
				continue;
131
			}
132
133 2
			// If this is anything but complex we need to do more cleaning!
134
			if ($cur_field['type'] !== 'callback' && $cur_field['type'] !== 'hidden')
135 2
			{
136
				if (!isset($cur_field['label']))
137
				{
138
					$cur_field['label'] = $txt[$field] ?? $field;
139
				}
140
141 2
				// Everything has a value!
142
				if (!isset($cur_field['value']))
143 2
				{
144
					$cur_field['value'] = $cur_profile[$field] ?? '';
145
				}
146
147 2
				// Any input attributes?
148
				$cur_field['input_attr'] = !empty($cur_field['input_attr']) ? implode(',', $cur_field['input_attr']) : '';
149
			}
150
151 2
			// Was there an error with this field on posting?
152
			if (isset($context['post_errors'][$field]))
153
			{
154
				$cur_field['is_error'] = true;
155
			}
156
157 2
			// Any javascript stuff?
158
			if (!empty($cur_field['js_submit']))
159
			{
160
				$context['profile_onsubmit_javascript'] .= $cur_field['js_submit'];
161 2
			}
162
			if (!empty($cur_field['js']))
163
			{
164
				theme()->addInlineJavascript($cur_field['js']);
165 2
			}
166
			if (!empty($cur_field['js_load']))
167
			{
168
				loadJavascriptFile($cur_field['js_load']);
169
			}
170
171 2
			// Any template stuff?
172
			if (!empty($cur_field['prehtml']))
173
			{
174
				$context['profile_prehtml'] .= $cur_field['prehtml'];
175 2
			}
176
			if (!empty($cur_field['posthtml']))
177
			{
178
				$context['profile_posthtml'] .= $cur_field['posthtml'];
179
			}
180
181 2
			// Finally, put it into context?
182
			if ($cur_field['type'] !== 'hidden')
183 2
			{
184 2
				$last_type = $cur_field['type'];
185
				$context['profile_fields'][$field] = &$profile_fields[$field];
186
			}
187
		}
188 2
		// Bodge in a line break - without doing two in a row ;)
189
		elseif ($field === 'hr' && $last_type !== 'hr' && $last_type !== '')
190 2
		{
191 2
			$last_type = 'hr';
192
			$context['profile_fields'][$i++]['type'] = 'hr';
193
		}
194
	}
195
196 2
	// Free up some memory.
197 2
	unset($profile_fields);
198
}
199
200
/**
201
 * Save the profile changes
202
 *
203
 * @param array $profile_vars
204
 * @param int $memID id_member
205
 */
206
function saveProfileChanges(&$profile_vars, $memID)
207
{
208
	global $context;
209
210 4
	// These make life easier....
211
	$old_id_theme = MembersList::get($memID)->id_theme;
0 ignored issues
show
Bug Best Practice introduced by
The property id_theme does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
212 4
213
	// Permissions...
214
	if ($context['user']['is_owner'])
215 4
	{
216 4
		$changeOther = allowedTo(array('profile_extra_any', 'profile_extra_own'));
217
	}
218
	else
219
	{
220
		$changeOther = allowedTo('profile_extra_any');
221
	}
222
223
	// Arrays of all the changes - makes things easier.
224
	$profile_bools = array(
225
		'notify_announcements',
226
		'notify_send_body',
227
	);
228
229 4
	$profile_ints = array(
230
		'notify_regularity',
231
		'notify_types',
232
		'notify_from',
233 4
	);
234
235 2
	$profile_floats = array();
236
237
	$profile_strings = array(
238
		'buddy_list',
239
		'ignore_boards',
240 4
	);
241
242
	call_integration_hook('integrate_save_profile_changes', array(&$profile_bools, &$profile_ints, &$profile_floats, &$profile_strings));
243
244
	if (isset($_POST['sa']) && $_POST['sa'] === 'ignoreboards' && empty($_POST['ignore_brd']))
245 4
	{
246
		$_POST['ignore_brd'] = array();
247
	}
248 4
249
	// Whatever it is set to is a dirty filthy thing.  Kinda like our minds.
250
	unset($_POST['ignore_boards']);
251 4
252 4
	if (isset($_POST['ignore_brd']))
253 4
	{
254 4
		if (!is_array($_POST['ignore_brd']))
255
		{
256
			$_POST['ignore_brd'] = array($_POST['ignore_brd']);
257 2
		}
258 2
259
		foreach ($_POST['ignore_brd'] as $k => $d)
260
		{
261 2
			$d = (int) $d;
262
			if ($d !== 0)
263
			{
264
				$_POST['ignore_brd'][$k] = $d;
265
			}
266
			else
267
			{
268
				unset($_POST['ignore_brd'][$k]);
269
			}
270
		}
271
		$_POST['ignore_boards'] = implode(',', $_POST['ignore_brd']);
272 2
		unset($_POST['ignore_brd']);
273
	}
274
275 2
	// Here's where we sort out all the 'other' values...
276
	if ($changeOther)
277
	{
278
		// Make any theme changes
279
		makeThemeChanges($memID, isset($_POST['id_theme']) ? (int) $_POST['id_theme'] : $old_id_theme);
280
281
		// Make any notification changes
282 2
		makeNotificationChanges($memID);
283
284
		if (!empty($_REQUEST['sa']))
285
		{
286
			makeCustomFieldChanges($memID, $_REQUEST['sa'], false);
287
		}
288
289
		foreach ($profile_bools as $var)
290
		{
291
			if (isset($_POST[$var]))
292
			{
293
				$profile_vars[$var] = empty($_POST[$var]) ? '0' : '1';
294
			}
295
		}
296
297
		foreach ($profile_ints as $var)
298
		{
299
			if (isset($_POST[$var]))
300
			{
301 2
				$profile_vars[$var] = $_POST[$var] != '' ? (int) $_POST[$var] : '';
302
			}
303 2
		}
304 2
305
		foreach ($profile_floats as $var)
306 2
		{
307
			if (isset($_POST[$var]))
308 2
			{
309 2
				$profile_vars[$var] = (float) $_POST[$var];
310 2
			}
311
		}
312 2
313 2
		foreach ($profile_strings as $var)
314
		{
315
			if (isset($_POST[$var]))
316 2
			{
317
				$profile_vars[$var] = $_POST[$var];
318
			}
319 2
		}
320
	}
321 2
}
322
323
/**
324
 * Make any theme changes that are sent with the profile.
325
 *
326
 * @param int $memID
327
 * @param int $id_theme
328
 * @throws \ElkArte\Exceptions\Exception
329
 */
330 2
function makeThemeChanges($memID, $id_theme)
331
{
332
	global $modSettings, $context;
333
334
	$db = database();
335 2
336
	$reservedVars = array(
337
		'actual_theme_url',
338
		'actual_images_url',
339
		'base_theme_dir',
340
		'base_theme_url',
341 2
		'default_images_url',
342
		'default_theme_dir',
343
		'default_theme_url',
344 2
		'default_template',
345 2
		'images_url',
346 2
		'number_recent_posts',
347 2
		'smiley_sets_default',
348
		'theme_dir',
349 2
		'theme_id',
350
		'theme_layers',
351 2
		'theme_templates',
352
		'theme_url',
353 2
	);
354
355
	// Can't change reserved vars.
356 2
	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))
357 2
	{
358
		throw new \ElkArte\Exceptions\Exception('no_access', false);
359 2
	}
360
361 2
	// Don't allow any overriding of custom fields with default or non-default options.
362
	$custom_fields = array();
363 2
	$db->fetchQuery('
364 2
		SELECT 
365 2
			col_name
366 2
		FROM {db_prefix}custom_fields
367 2
		WHERE active = {int:is_active}',
368 2
		array(
369 2
			'is_active' => 1,
370 2
		)
371 2
	)->fetch_callback(
372 2
		function ($row) use (&$custom_fields) {
373 2
			$custom_fields[] = $row['col_name'];
374 2
		}
375
	);
376
377
	// These are the theme changes...
378 4
	$themeSetArray = array();
379
	if (isset($_POST['options']) && is_array($_POST['options']))
380 4
	{
381 4
		foreach ($_POST['options'] as $opt => $val)
382
		{
383
			if (in_array($opt, $custom_fields))
384
			{
385
				continue;
386
			}
387
388
			// These need to be controlled.
389
			if ($opt === 'topics_per_page' || $opt === 'messages_per_page')
390
			{
391 2
				$val = max(0, min($val, 50));
392
			}
393
			// We don't set this per theme anymore.
394 2
			elseif ($opt === 'allow_no_censored')
395
			{
396
				continue;
397
			}
398
399
			$themeSetArray[] = array($id_theme, $memID, $opt, is_array($val) ? implode(',', $val) : $val);
400
		}
401
	}
402
403
	$erase_options = array();
404
	if (isset($_POST['default_options']) && is_array($_POST['default_options']))
405
	{
406
		foreach ($_POST['default_options'] as $opt => $val)
407
		{
408
			if (in_array($opt, $custom_fields))
409
			{
410
				continue;
411
			}
412
413
			// These need to be controlled.
414
			if ($opt === 'topics_per_page' || $opt === 'messages_per_page')
415
			{
416
				$val = max(0, min($val, 50));
417
			}
418
			// Only let admins and owners change the censor.
419
			elseif ($opt === 'allow_no_censored' && User::$info->is_admin && !$context['user']['is_owner'])
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
420
			{
421
				continue;
422
			}
423
424
			$themeSetArray[] = array(1, $memID, $opt, is_array($val) ? implode(',', $val) : $val);
425
			$erase_options[] = $opt;
426
		}
427
	}
428
429
	// If themeSetArray isn't still empty, send it to the database.
430
	if (empty($context['password_auth_failed']))
431
	{
432
		require_once(SUBSDIR . '/Themes.subs.php');
433
		if (!empty($themeSetArray))
434
		{
435
			updateThemeOptions($themeSetArray);
436
		}
437
438
		if (!empty($erase_options))
439
		{
440
			removeThemeOptions('custom', $memID, $erase_options);
441
		}
442
443
		$themes = explode(',', $modSettings['knownThemes']);
444
		foreach ($themes as $t)
445
		{
446 1
			Cache::instance()->remove('theme_settings-' . $t . ':' . $memID);
447 1
		}
448
	}
449
}
450
451
/**
452
 * Make any notification changes that need to be made.
453
 *
454
 * @param int $memID id_member
455 2
 */
456 2
function makeNotificationChanges($memID)
457 2
{
458
	$db = database();
459
460
	if (isset($_POST['notify_submit']))
461
	{
462
		$to_save = [];
463
464
		foreach (getMemberNotificationsProfile($memID) as $mention => $data)
465
		{
466
			if (isset($_POST['notify'][$mention]) && !empty($_POST['notify'][$mention]['status']))
467
			{
468
				// When is not an array it means => use default => 0, so it's skipped on INSERT
469
				if (!is_array($_POST['notify'][$mention]['status']))
470 2
				{
471
					$to_save[$mention] = 0;
472
					continue;
473
				}
474
475
				foreach ($_POST['notify'][$mention]['status'] as $method)
476
				{
477
					// This ensures that the $method passed by the user is valid and safe to INSERT.
478
					if (isset($data['data'][$method]))
479
					{
480
						if (!isset($to_save[$mention]))
481
						{
482
							$to_save[$mention] = [];
483
						}
484
						$to_save[$mention][] = $method;
485
					}
486
				}
487
			}
488
			else
489
			{
490
				$to_save[$mention] = [Notifications::DEFAULT_NONE];
491
492
				// Shhh. ... Board/Topic onsite notifications are added as typical mentions on the fly
493
				$to_save['watchedtopic'] = 0;
494
				$to_save['watchedboard'] = 0;
495 2
				if ((int) $_POST['notify_regularity'] === 4)
496
				{
497
					$to_save['watchedtopic'] = [0 => 'notification'];
498
					$to_save['watchedboard'] = [0 => 'notification'];
499 2
				}
500 2
			}
501
		}
502
503
		saveUserNotificationsPreferences($memID, $to_save);
504
	}
505
506
	// Update the boards they are being notified on.
507
	if (isset($_POST['edit_notify_boards']))
508
	{
509
		if (!isset($_POST['notify_boards']))
510
		{
511
			$_POST['notify_boards'] = array();
512
		}
513
514
		// Make sure only integers are added/deleted.
515
		foreach ($_POST['notify_boards'] as $index => $id)
516
		{
517 2
			$_POST['notify_boards'][$index] = (int) $id;
518
		}
519
520 2
		// id_board = 0 is reserved for topic notifications only
521 2
		$notification_wanted = array_diff($_POST['notify_boards'], array(0));
522 2
523
		// Gather up any any existing board notifications.
524 2
		$notification_current = array();
525
		$db->fetchQuery('
526
			SELECT 
527
				id_board
528
			FROM {db_prefix}log_notify
529
			WHERE id_member = {int:selected_member}
530
				AND id_board != {int:id_board}',
531
			array(
532
				'selected_member' => $memID,
533
				'id_board' => 0,
534
			)
535
		)->fetch_callback(
536
			function ($row) use (&$notification_current) {
537
				$notification_current[] = $row['id_board'];
538
			}
539
		);
540
541
		// And remove what they no longer want
542
		$notification_deletes = array_diff($notification_current, $notification_wanted);
543
		if (!empty($notification_deletes))
544
		{
545
			$db->query('', '
546 2
				DELETE FROM {db_prefix}log_notify
547
				WHERE id_board IN ({array_int:board_list})
548
					AND id_member = {int:selected_member}',
549 2
				array(
550 2
					'board_list' => $notification_deletes,
551 2
					'selected_member' => $memID,
552
				)
553 2
			);
554
		}
555
556
		// Now add in what they do want
557
		$notification_inserts = array();
558
		foreach ($notification_wanted as $id)
559
		{
560
			$notification_inserts[] = array($memID, $id);
561
		}
562
563
		if (!empty($notification_inserts))
564
		{
565
			$db->insert('ignore',
566
				'{db_prefix}log_notify',
567
				array('id_member' => 'int', 'id_board' => 'int'),
568
				$notification_inserts,
569
				array('id_member', 'id_board')
570
			);
571
		}
572
	}
573
	// We are editing topic notifications......
574
	elseif (isset($_POST['edit_notify_topics']) && !empty($_POST['notify_topics']))
575
	{
576 2
		$edit_notify_topics = array();
577
		foreach ($_POST['notify_topics'] as $index => $id)
578
		{
579 2
			$edit_notify_topics[$index] = (int) $id;
580 2
		}
581 2
582 2
		// Make sure there are no zeros left.
583
		$edit_notify_topics = array_diff($edit_notify_topics, array(0));
584
585
		$db->query('', '
586
			DELETE FROM {db_prefix}log_notify
587 2
			WHERE id_topic IN ({array_int:topic_list})
588
				AND id_member = {int:selected_member}',
589
			array(
590
				'topic_list' => $edit_notify_topics,
591
				'selected_member' => $memID,
592
			)
593
		);
594
	}
595
}
596
597
/**
598
 * Save any changes to the custom profile fields
599 2
 *
600 2
 * @param int $memID
601 2
 * @param string $area
602 2
 * @param bool $sanitize = true
603
 */
604
function makeCustomFieldChanges($memID, $area, $sanitize = true)
605
{
606
	global $context, $modSettings;
607
608
	$db = database();
609
610
	if ($sanitize && isset($_POST['customfield']))
611
	{
612
		$_POST['customfield'] = Util::htmlspecialchars__recursive($_POST['customfield']);
613
	}
614
615
	$where = $area === 'register' ? 'show_reg != 0' : 'show_profile = {string:area}';
616
617
	// Load the fields we are saving too - make sure we save valid data (etc).
618
	$request = $db->query('', '
619
		SELECT 
620
			col_name, field_name, field_desc, field_type, field_length, field_options, default_value, show_reg, mask, private
621
		FROM {db_prefix}custom_fields
622
		WHERE ' . $where . '
623
			AND active = {int:is_active}',
624
		array(
625
			'is_active' => 1,
626
			'area' => $area,
627 2
		)
628
	);
629
	$changes = array();
630
	$log_changes = array();
631
	while (($row = $request->fetch_assoc()))
632 2
	{
633
		/* This means don't save if:
634
			- The user is NOT an admin.
635 2
			- The data is not freely viewable and editable by users.
636 2
			- The data is not invisible to users but editable by the owner (or if it is the user is not the owner)
637 2
			- The area isn't registration, and if it is that the field is not supposed to be shown there.
638
		*/
639
		if ($row['private'] != 0 && !allowedTo('admin_forum') && ($memID != User::$info->id || $row['private'] != 2) && ($area !== 'register' || $row['show_reg'] == 0))
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
640
		{
641
			continue;
642
		}
643
644
		// Validate the user data.
645
		if ($row['field_type'] === 'check')
646
		{
647
			$value = isset($_POST['customfield'][$row['col_name']]) ? 1 : 0;
648
		}
649
		elseif (in_array($row['field_type'], array('radio', 'select')))
650 2
		{
651
			$value = $row['default_value'];
652
			$options = explode(',', $row['field_options']);
653
654
			foreach ($options as $k => $v)
655
			{
656
				if (isset($_POST['customfield'][$row['col_name']]) && $_POST['customfield'][$row['col_name']] == $k)
657
				{
658
					$key = $k;
659
					$value = $v;
660 2
				}
661 2
			}
662
		}
663
		// Otherwise some form of text!
664 2
		else
665 2
		{
666 2
			// TODO: This is a bit backwards.
667 2
			$value = $_POST['customfield'][$row['col_name']] ?? $row['default_value'];
668 2
			$is_valid = isCustomFieldValid($row, $value);
669 2
670 2
			if ($is_valid !== true)
671
			{
672
				switch ($is_valid)
673
				{
674
					case 'custom_field_too_long':
675
						$value = Util::substr($value, 0, $row['field_length']);
676
						break;
677
					case 'custom_field_invalid_email':
678
					case 'custom_field_inproper_format':
679
						$value = $row['default_value'];
680
						break;
681
				}
682
			}
683
684
			if ($row['mask'] === 'number')
685
			{
686
				$value = (int) $value;
687
			}
688
		}
689
690
		$options = MembersList::get($memID)->options;
0 ignored issues
show
Bug Best Practice introduced by
The property options does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
691
		// Did it change or has it been set?
692 2
		if ((!isset($options[$row['col_name']]) && !empty($value)) || (isset($options[$row['col_name']]) && $options[$row['col_name']] !== $value))
693
		{
694
			$log_changes[] = array(
695
				'action' => 'customfield_' . $row['col_name'],
696 2
				'log_type' => 'user',
697 2
				'extra' => array(
698 2
					'previous' => !empty($options[$row['col_name']]) ? $options[$row['col_name']] : '',
699
					'new' => $value,
700 2
					'applicator' => User::$info->id,
701 2
					'member_affected' => $memID,
702
				),
703
			);
704
705
			$changes[] = array($row['col_name'], $value, $memID);
706
			if (in_array($row['field_type'], array('radio', 'select')))
707
			{
708
				$options[$row['col_name']] = $value;
709
				$options[$row['col_name'] . '_key'] = $row['col_name'] . '_' . ($key ?? 0);
710
			}
711
			else
712
			{
713
				$options[$row['col_name']] = $value;
714
			}
715
		}
716
	}
717
	$request->free_result();
718
719
	call_integration_hook('integrate_save_custom_profile_fields', array(&$changes, &$log_changes, $memID, $area, $sanitize));
720
721
	// Make those changes!
722
	if (!empty($changes) && empty($context['password_auth_failed']))
723
	{
724
		$db->replace(
725
			'{db_prefix}custom_fields_data',
726
			array('variable' => 'string-255', 'value' => 'string-65534', 'id_member' => 'int'),
727
			$changes,
728
			array('variable', 'id_member')
729
		);
730
731
		if (!empty($log_changes) && featureEnabled('ml') && !empty($modSettings['userlog_enabled']))
732
		{
733
			logActions($log_changes);
734
		}
735
	}
736
}
737
738
/**
739
 * Validates the value of a custom field
740
 *
741
 * @param array $field - An array describing the field. It consists of the
742
 *                indexes:
743
 *                  - type; if different from 'text', only the length is checked
744
 *                  - mask; if empty or equal to 'none', only the length is
745
 *                          checked, possible masks are: email, number, regex
746
 *                  - field_length; maximum length of the field
747 2
 * @param string|int $value - The value that we want to validate
748
 * @return string|bool - A string representing the type of error, or true
749
 */
750 2
function isCustomFieldValid($field, $value)
751 2
{
752 2
	// Is it too long?
753 2
	if ($field['field_length'] && $field['field_length'] < Util::strlen($value))
754 2
	{
755 2
		return 'custom_field_too_long';
756 2
	}
757 2
758
	// Any masks to apply?
759
	if ($field['field_type'] === 'text' && !empty($field['mask']) && $field['mask'] !== 'none')
760
	{
761
		if ($field['mask'] === 'email' && !isValidEmail($value))
762
		{
763
			return 'custom_field_invalid_email';
764
		}
765
766
		if ($field['mask'] === 'number' && preg_match('~\D~', $value))
767
		{
768
			return 'custom_field_not_number';
769
		}
770
771
		if (strpos($field['mask'], 'regex') === 0 && trim($value) !== '' && preg_match(substr($field['mask'], 5), $value) === 0)
772
		{
773
			return 'custom_field_inproper_format';
774
		}
775
	}
776
777
	return true;
778
}
779
780
/**
781
 * Send the user a new activation email if they need to reactivate!
782
 */
783
function profileSendActivation()
784
{
785
	global $profile_vars, $old_profile, $txt, $context, $scripturl, $cookiename, $cur_profile, $language, $modSettings;
786
787
	require_once(SUBSDIR . '/Mail.subs.php');
788
789 2
	// Shouldn't happen but just in case.
790
	if (empty($profile_vars['email_address']))
791
	{
792 2
		return;
793 2
	}
794 2
795 2
	$replacements = array(
796 2
		'ACTIVATIONLINK' => $scripturl . '?action=register;sa=activate;u=' . $context['id_member'] . ';code=' . $old_profile['validation_code'],
797 2
		'ACTIVATIONCODE' => $old_profile['validation_code'],
798
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=register;sa=activate;u=' . $context['id_member'],
799
	);
800
801 2
	// Send off the email.
802 2
	$emaildata = loadEmailTemplate('activate_reactivate', $replacements, empty($cur_profile['lngfile']) || empty($modSettings['userLanguage']) ? $language : $cur_profile['lngfile']);
803 2
	sendmail($profile_vars['email_address'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
804 2
805 2
	// Log the user out.
806
	require_once(SUBSDIR . '/Logging.subs.php');
807
	logOnline($context['id_member'], false);
808 2
	$_SESSION['log_time'] = 0;
809 2
	$_SESSION['login_' . $cookiename] = serialize(array(0, '', 0));
810 2
811 2
	if (isset($_COOKIE[$cookiename]))
812 2
	{
813 2
		$_COOKIE[$cookiename] = '';
814 2
	}
815 2
816
	User::load(true);
817
	User::$info['is_logged'] = $context['user']['is_logged'] = false;
818
	User::$info['is_guest'] = $context['user']['is_guest'] = true;
819 2
820
	// Send them to the done-with-registration-login screen.
821 2
	theme()->getTemplates()->load('Register');
822 2
823 2
	$context['page_title'] = $txt['profile'];
824 2
	$context['sub_template'] = 'after';
825
	$context['title'] = $txt['activate_changed_email_title'];
826 2
	$context['description'] = $txt['activate_changed_email_desc'];
827 2
828
	// We're gone!
829
	obExit();
830
}
831
832
/**
833
 * Load key signature context data.
834
 *
835
 * @return bool
836
 */
837 2
function profileLoadSignatureData()
838
{
839
	global $modSettings, $context, $txt, $cur_profile;
840
841 2
	// Signature limits.
842 2
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
843 2
	$sig_limits = explode(',', $sig_limits);
844 2
845
	$context['signature_enabled'] = $sig_limits[0] ?? 0;
846
	$context['signature_limits'] = array(
847
		'max_length' => $sig_limits[1] ?? 0,
848
		'max_lines' => $sig_limits[2] ?? 0,
849
		'max_images' => $sig_limits[3] ?? 0,
850
		'max_smileys' => $sig_limits[4] ?? 0,
851
		'max_image_width' => $sig_limits[5] ?? 0,
852 2
		'max_image_height' => $sig_limits[6] ?? 0,
853
		'max_font_size' => $sig_limits[7] ?? 0,
854
		'bbc' => !empty($sig_bbc) ? explode(',', $sig_bbc) : array(),
855
	);
856
857
	// Warning message for signature image limits?
858
	$context['signature_warning'] = '';
859
	if ($context['signature_limits']['max_image_width'] && $context['signature_limits']['max_image_height'])
860
	{
861
		$context['signature_warning'] = sprintf($txt['profile_error_signature_max_image_size'], $context['signature_limits']['max_image_width'], $context['signature_limits']['max_image_height']);
862 2
	}
863
	elseif ($context['signature_limits']['max_image_width'] || $context['signature_limits']['max_image_height'])
864
	{
865 2
		$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']);
866 2
	}
867
868 2
	if (empty($context['do_preview']))
869 2
	{
870
		$context['member']['signature'] = empty($cur_profile['signature_raw']) ? '' : str_replace(array('<br />', '<', '>', '"', '\''), array("\n", '&lt;', '&gt;', '&quot;', '&#039;'), $cur_profile['signature_raw']);
871
	}
872
	else
873
	{
874
		$signature = !empty($_POST['signature']) ? $_POST['signature'] : '';
875
		$validation = profileValidateSignature($signature);
876
		if (empty($context['post_errors']))
877
		{
878
			Txt::load('Errors');
879
			$context['post_errors'] = array();
880
		}
881 2
882
		$context['post_errors'][] = 'signature_not_yet_saved';
883
		if ($validation !== true && $validation !== false)
884 2
		{
885 2
			$context['post_errors'][] = $validation;
886 2
		}
887
888
		$context['member']['signature'] = censor($context['member']['signature']);
889 2
		$context['member']['current_signature'] = $context['member']['signature'];
890 2
		$signature = censor($signature);
891
		$bbc_parser = ParserWrapper::instance();
892
		$context['member']['signature_preview'] = $bbc_parser->parseSignature($signature, true);
893
		$context['member']['signature'] = $_POST['signature'];
894
	}
895
896
	return true;
897
}
898
899
/**
900
 * Load avatar context data.
901
 *
902
 * @return bool
903
 */
904
function profileLoadAvatarData()
905
{
906
	global $context, $cur_profile, $modSettings;
907
908
	if (!is_array($cur_profile['avatar']))
909
	{
910
		$cur_profile['avatar'] = determineAvatar($cur_profile);
911
	}
912
913
	$context['avatar_url'] = $modSettings['avatar_url'];
914 2
	$valid_protocol = preg_match('~^https' . (detectServer()->supportsSSL() ? '' : '?') . '://~i', $cur_profile['avatar']['name']) === 1;
915
	$schema = 'http' . (detectServer()->supportsSSL() ? 's' : '') . '://';
916
917 2
	// @todo Temporary
918 2
	if ($context['user']['is_owner'])
919 2
	{
920 2
		$allowedChange = allowedTo('profile_set_avatar') && allowedTo(array('profile_extra_any', 'profile_extra_own'));
921 2
	}
922
	else
923
	{
924 2
		$allowedChange = allowedTo('profile_set_avatar') && allowedTo('profile_extra_any');
925 2
	}
926 2
927 2
	// Default context.
928 2
	$context['member']['avatar'] += array(
929 2
		'custom' => $valid_protocol ? $cur_profile['avatar']['name'] : $schema,
930 2
		'selection' => $valid_protocol ? $cur_profile['avatar']['name'] : '',
931
		'id_attach' => $cur_profile['id_attach'],
932
		'filename' => $cur_profile['filename'],
933
		'allow_server_stored' => !empty($modSettings['avatar_stored_enabled']) && $allowedChange,
934
		'allow_upload' => !empty($modSettings['avatar_upload_enabled']) && $allowedChange,
935
		'allow_external' => !empty($modSettings['avatar_external_enabled']) && $allowedChange,
936
		'allow_gravatar' => !empty($modSettings['avatar_gravatar_enabled']) && $allowedChange,
937
	);
938
939
	if ($cur_profile['avatar']['name'] === '' && $cur_profile['id_attach'] > 0 && $context['member']['avatar']['allow_upload'])
940
	{
941
		$context['member']['avatar'] += array(
942
			'choice' => 'upload',
943
			'server_pic' => 'blank.png',
944
			'external' => '',
945
			'placeholder' => $schema
946
		);
947
948
		$context['member']['avatar'] += array(
949
			'href' => empty($cur_profile['attachment_type'])
950
				? getUrl('attach', ['action' => 'dlattach', 'attach' => (int) $cur_profile['id_attach'], 'name' => $cur_profile['filename'], 'type' => 'avatar'])
951
				: $modSettings['custom_avatar_url'] . '/' . $cur_profile['filename']
952
		);
953
	}
954
	elseif ($valid_protocol && $context['member']['avatar']['allow_external'])
955
	{
956 2
		$context['member']['avatar'] += array(
957
			'choice' => 'external',
958
			'server_pic' => 'blank.png',
959 2
			'external' => $cur_profile['avatar']['name']
960 2
		);
961 2
	}
962 2
	elseif ($cur_profile['avatar']['name'] === 'gravatar' && $context['member']['avatar']['allow_gravatar'])
963 2
	{
964 2
		$context['member']['avatar'] += array(
965
			'choice' => 'gravatar',
966
			'server_pic' => 'blank.png',
967 2
			'external' => 'https://'
968 2
		);
969 2
	}
970 2
	elseif ($cur_profile['avatar']['name'] !== '' && FileFunctions::instance()->fileExists($modSettings['avatar_directory'] . '/' . $cur_profile['avatar']['name']) && $context['member']['avatar']['allow_server_stored'])
971
	{
972
		$context['member']['avatar'] += array(
973 2
			'choice' => 'server_stored',
974 2
			'server_pic' => $cur_profile['avatar']['name'],
975 2
			'external' => $schema
976 2
		);
977
	}
978
	else
979
	{
980
		$context['member']['avatar'] += array(
981
			'choice' => 'none',
982
			'server_pic' => 'blank.png',
983
			'external' => $schema
984
		);
985
	}
986
987
	// Get a list of all the avatars.
988
	if ($context['member']['avatar']['allow_server_stored'])
989
	{
990
		require_once(SUBSDIR . '/Attachments.subs.php');
991
		$context['avatar_list'] = array();
992
		$context['avatars'] = FileFunctions::instance()->isDir($modSettings['avatar_directory']) ? getServerStoredAvatars('') : array();
993
	}
994
	else
995
	{
996
		$context['avatar_list'] = array();
997
		$context['avatars'] = array();
998
	}
999
1000
	// Second level selected avatar...
1001
	$context['avatar_selected'] = substr(strrchr($context['member']['avatar']['server_pic'], '/'), 1);
1002
1003
	return true;
1004
}
1005 2
1006
/**
1007
 * Loads all the member groups that this member can assign
1008
 * Places the result in context for template use
1009
 */
1010
function profileLoadGroups()
1011
{
1012
	global $cur_profile, $context;
1013
1014
	require_once(SUBSDIR . '/Membergroups.subs.php');
1015
1016 2
	$context['member_groups'] = getGroupsList();
1017
	$context['member_groups'][0]['is_primary'] = (int) $cur_profile['id_group'] === 0;
1018
1019
	$curGroups = explode(',', $cur_profile['additional_groups']);
1020 2
	$curGroups = array_map('intval', $curGroups);
1021 2
1022 2
	foreach ($context['member_groups'] as $id_group => $row)
1023
	{
1024
		$id_group = (int) $id_group;
1025
		// Registered member was already taken care before
1026
		if ($id_group === 0)
1027
		{
1028
			continue;
1029
		}
1030
1031
		$context['member_groups'][$id_group]['is_primary'] = $cur_profile['id_group'] == $id_group;
1032
		$context['member_groups'][$id_group]['is_additional'] = in_array($id_group, $curGroups, true);
1033
		$context['member_groups'][$id_group]['can_be_additional'] = true;
1034
		$context['member_groups'][$id_group]['can_be_primary'] = $row['hidden'] != 2;
1035
	}
1036
1037 2
	$context['member']['group_id'] = User::$settings['id_group'];
1038
1039
	return true;
1040 2
}
1041 2
1042 2
/**
1043
 * Load all the languages for the profile.
1044
 */
1045
function profileLoadLanguages()
1046
{
1047
	global $context;
1048
1049
	$context['profile_languages'] = array();
1050
1051
	// Get our languages!
1052
	$languages = getLanguages();
1053
1054
	// Setup our languages.
1055
	foreach ($languages as $lang)
1056
	{
1057
		$context['profile_languages'][$lang['filename']] = $lang['name'];
1058
	}
1059
1060
	ksort($context['profile_languages']);
1061 2
1062
	// Return whether we should proceed with this.
1063
	return count($context['profile_languages']) > 1;
1064 2
}
1065 2
1066 2
/**
1067
 * Reload a users settings.
1068
 */
1069
function profileReloadUser()
1070
{
1071
	global $modSettings, $context, $cur_profile;
1072
1073 2
	// Log them back in - using the verify password as they must have matched and this one doesn't get changed by anyone!
1074
	if (isset($_POST['passwrd2']) && $_POST['passwrd2'] !== '')
1075
	{
1076
		require_once(SUBSDIR . '/Auth.subs.php');
1077
		$check = validateLoginPassword($_POST['passwrd2'], $_POST['passwrd1'], $cur_profile['member_name']);
1078
		if ($check === true)
1079
		{
1080
			setLoginCookie(60 * $modSettings['cookieTime'], $context['id_member'], hash('sha256', $_POST['passwrd1'] . $cur_profile['password_salt']));
1081
		}
1082
	}
1083
1084 2
	User::load(true);
1085
	writeLog();
1086
}
1087 2
1088 2
/**
1089
 * Validate the signature
1090
 *
1091 2
 * @param string $value
1092 2
 *
1093 2
 * @return bool|string
1094
 */
1095
function profileValidateSignature(&$value)
1096
{
1097
	global $modSettings, $txt;
1098
1099
	require_once(SUBSDIR . '/Post.subs.php');
1100
1101 2
	// Admins can do whatever they hell they want!
1102
	if (!allowedTo('admin_forum'))
1103
	{
1104 2
		// Load all the signature limits.
1105 2
		list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
1106 2
		$sig_limits = explode(',', $sig_limits);
1107 2
		$disabledTags = !empty($sig_bbc) ? explode(',', $sig_bbc) : array();
1108 2
1109 2
		$unparsed_signature = strtr(un_htmlspecialchars($value), array("\r" => '', '&#039' => '\''));
1110
1111
		// Too many lines?
1112 2
		if (!empty($sig_limits[2]) && substr_count($unparsed_signature, "\n") >= $sig_limits[2])
1113 2
		{
1114 2
			$txt['profile_error_signature_max_lines'] = sprintf($txt['profile_error_signature_max_lines'], $sig_limits[2]);
1115 2
1116 2
			return 'signature_max_lines';
1117
		}
1118
1119
		// Too many images?!
1120
		if (!empty($sig_limits[3]) && (substr_count(strtolower($unparsed_signature), '[img') + substr_count(strtolower($unparsed_signature), '<img')) > $sig_limits[3])
1121
		{
1122
			$txt['profile_error_signature_max_image_count'] = sprintf($txt['profile_error_signature_max_image_count'], $sig_limits[3]);
1123
1124
			return 'signature_max_image_count';
1125
		}
1126
1127 2
		// What about too many smileys!
1128 2
		$smiley_parsed = $unparsed_signature;
1129
		$wrapper = ParserWrapper::instance();
1130
		$parser = $wrapper->getSmileyParser();
1131
		$parser->setEnabled($GLOBALS['user_info']['smiley_set'] !== 'none' && trim($smiley_parsed) !== '');
1132 2
		$smiley_parsed = $parser->parseBlock($smiley_parsed);
1133
1134 2
		$smiley_count = substr_count(strtolower($smiley_parsed), '<img') - substr_count(strtolower($unparsed_signature), '<img');
1135
		if (!empty($sig_limits[4]) && $sig_limits[4] == -1 && $smiley_count > 0)
1136
		{
1137 2
			return 'signature_allow_smileys';
1138
		}
1139
1140 2
		if (!empty($sig_limits[4]) && $sig_limits[4] > 0 && $smiley_count > $sig_limits[4])
1141
		{
1142
			$txt['profile_error_signature_max_smileys'] = sprintf($txt['profile_error_signature_max_smileys'], $sig_limits[4]);
1143 2
1144
			return 'signature_max_smileys';
1145
		}
1146
1147
		// Maybe we are abusing font sizes?
1148
		if (!empty($sig_limits[7]) && preg_match_all('~\[size=([\d\.]+)(\]|px|pt|em|x-large|larger)~i', $unparsed_signature, $matches) !== false)
1149 2
		{
1150
			// Same as parse_bbc
1151 2
			$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
1152
1153
			foreach ($matches[1] as $ind => $size)
1154
			{
1155 2
				$limit_broke = 0;
1156
1157 1
				// Just specifying as [size=x]?
1158
				if (empty($matches[2][$ind]))
1159
				{
1160 2
					$matches[2][$ind] = 'em';
1161
					$size = $sizes[(int) $size] ?? 0;
1162
				}
1163
1164
				// Attempt to allow all sizes of abuse, so to speak.
1165
				if ($matches[2][$ind] === 'px' && $size > $sig_limits[7])
1166
				{
1167
					$limit_broke = $sig_limits[7] . 'px';
1168
				}
1169
				elseif ($matches[2][$ind] === 'pt' && $size > ($sig_limits[7] * 0.75))
1170
				{
1171
					$limit_broke = ((int) $sig_limits[7] * 0.75) . 'pt';
1172
				}
1173
				elseif ($matches[2][$ind] === 'em' && $size > ((float) $sig_limits[7] / 14))
1174
				{
1175
					$limit_broke = ((float) $sig_limits[7] / 14) . 'em';
1176
				}
1177
				elseif ($matches[2][$ind] !== 'px' && $matches[2][$ind] !== 'pt' && $matches[2][$ind] !== 'em' && $sig_limits[7] < 18)
1178
				{
1179
					$limit_broke = 'large';
1180
				}
1181
1182
				if ($limit_broke)
1183
				{
1184
					$txt['profile_error_signature_max_font_size'] = sprintf($txt['profile_error_signature_max_font_size'], $limit_broke);
1185
1186
					return 'signature_max_font_size';
1187
				}
1188
			}
1189
		}
1190
1191
		// The difficult one - image sizes! Don't error on this - just fix it.
1192
		if ((!empty($sig_limits[5]) || !empty($sig_limits[6])))
1193
		{
1194
			// Get all BBC tags...
1195
			preg_match_all('~\[img(\s+width=([\d]+))?(\s+height=([\d]+))?(\s+width=([\d]+))?\s*\](?:<br />)*([^<">]+?)(?:<br />)*\[/img\]~i', $unparsed_signature, $matches);
1196
1197
			// ... and all HTML ones.
1198
			preg_match_all('~<img\s+src=(?:")?((?:http://|ftp://|https://|ftps://).+?)(?:")?(?:\s+alt=(?:")?(.*?)(?:")?)?(?:\s?/)?>~i', $unparsed_signature, $matches2, PREG_PATTERN_ORDER);
1199
1200
			// And stick the HTML in the BBC.
1201
			if (!empty($matches2))
1202
			{
1203
				foreach ($matches2[0] as $ind => $dummy)
1204
				{
1205
					$matches[0][] = $matches2[0][$ind];
1206
					$matches[1][] = '';
1207
					$matches[2][] = '';
1208
					$matches[3][] = '';
1209
					$matches[4][] = '';
1210
					$matches[5][] = '';
1211
					$matches[6][] = '';
1212
					$matches[7][] = $matches2[1][$ind];
1213
				}
1214
			}
1215
1216
			$replaces = array();
1217
1218
			// Try to find all the images!
1219
			if (!empty($matches))
1220
			{
1221
				foreach ($matches[0] as $key => $image)
1222
				{
1223
					$width = -1;
1224
					$height = -1;
1225
1226
					// Does it have predefined restraints? Width first.
1227
					if ($matches[6][$key])
1228
					{
1229
						$matches[2][$key] = $matches[6][$key];
1230
					}
1231
1232
					if ($matches[2][$key] && $sig_limits[5] && $matches[2][$key] > $sig_limits[5])
1233
					{
1234
						$width = $sig_limits[5];
1235
						$matches[4][$key] *= $width / $matches[2][$key];
1236
					}
1237
					elseif ($matches[2][$key])
1238
					{
1239
						$width = $matches[2][$key];
1240
					}
1241
1242
					// ... and height.
1243
					if ($matches[4][$key] && $sig_limits[6] && $matches[4][$key] > $sig_limits[6])
1244
					{
1245
						$height = $sig_limits[6];
1246
						if ($width != -1)
1247
						{
1248
							$width *= $height / $matches[4][$key];
1249
						}
1250
					}
1251
					elseif ($matches[4][$key])
1252
					{
1253
						$height = $matches[4][$key];
1254
					}
1255
1256
					// If the dimensions are still not fixed - we need to check the actual image.
1257
					if (($width == -1 && $sig_limits[5]) || ($height == -1 && $sig_limits[6]))
1258
					{
1259
						require_once(SUBSDIR . '/Attachments.subs.php');
1260
						$sizes = url_image_size($matches[7][$key]);
1261
						if (is_array($sizes))
1262
						{
1263
							// Too wide?
1264
							if ($sizes[0] > $sig_limits[5] && $sig_limits[5])
1265
							{
1266
								$width = $sig_limits[5];
1267
								$sizes[1] *= $width / $sizes[0];
1268
							}
1269
1270
							// Too high?
1271
							if ($sizes[1] > $sig_limits[6] && $sig_limits[6])
1272
							{
1273
								$height = $sig_limits[6];
1274
								if ($width == -1)
1275
								{
1276
									$width = $sizes[0];
1277
								}
1278
								$width *= $height / $sizes[1];
1279
							}
1280
							elseif ($width != -1)
1281
							{
1282
								$height = $sizes[1];
1283
							}
1284
						}
1285
					}
1286
1287
					// Did we come up with some changes? If so remake the string.
1288
					if ($width != -1 || $height != -1)
1289
					{
1290
						$replaces[$image] = '[img' . ($width != -1 ? ' width=' . round($width) : '') . ($height != -1 ? ' height=' . round($height) : '') . ']' . $matches[7][$key] . '[/img]';
1291
					}
1292
				}
1293
1294
				if (!empty($replaces))
1295
				{
1296
					$value = str_replace(array_keys($replaces), array_values($replaces), $value);
1297
				}
1298
			}
1299
		}
1300
1301
		// @todo temporary, footnotes in signatures is not available at this time
1302
		$disabledTags[] = 'footnote';
1303
1304
		// Any disabled BBC?
1305
		$disabledSigBBC = implode('|', $disabledTags);
1306
1307
		if (!empty($disabledSigBBC))
1308
		{
1309
			if (preg_match('~\[(' . $disabledSigBBC . '[ =\]/])~i', $unparsed_signature, $matches) !== false && isset($matches[1]))
1310
			{
1311
				$disabledTags = array_unique($disabledTags);
1312
				$txt['profile_error_signature_disabled_bbc'] = sprintf($txt['profile_error_signature_disabled_bbc'], implode(', ', $disabledTags));
1313
1314
				return 'signature_disabled_bbc';
1315
			}
1316
		}
1317
	}
1318
1319
	preparsecode($value);
1320
1321
	// Too long?
1322
	if (!allowedTo('admin_forum') && !empty($sig_limits[1]) && Util::strlen(str_replace('<br />', "\n", $value)) > $sig_limits[1])
1323
	{
1324
		$_POST['signature'] = trim(htmlspecialchars(str_replace('<br />', "\n", $value), ENT_QUOTES, 'UTF-8'));
1325
		$txt['profile_error_signature_max_length'] = sprintf($txt['profile_error_signature_max_length'], $sig_limits[1]);
1326
1327
		return 'signature_max_length';
1328
	}
1329
1330
	return true;
1331
}
1332
1333
/**
1334
 * The avatar is incredibly complicated, what with the options... and what not.
1335
 *
1336
 * @param string $value
1337
 *
1338
 * @return false|string
1339
 *
1340
 */
1341
function profileSaveAvatarData($value)
1342
{
1343
	global $profile_vars, $cur_profile, $context;
1344
1345
	$memID = $context['id_member'];
1346
	if (empty($memID) && !empty($context['password_auth_failed']))
1347
	{
1348
		return false;
1349
	}
1350
1351
	$avatar = new Avatars();
1352
	$result = $avatar->processValue($value);
1353
	if ($result !== true)
1354
	{
1355
		$profile_vars['avatar'] = '';
1356
1357
		// @todo add some specific errors to the class
1358
		$errors = ErrorContext::context('profile', 0);
1359
		$errors->addError($result);
1360
		$context['post_errors'] = array(
1361
			'errors' => $errors->prepareErrors(),
1362
			'type' => $errors->getErrorType() == 0 ? 'minor' : 'serious',
1363
		);
1364
1365
		return $result;
1366
	}
1367
1368
	// Setup the profile variables so it shows things right on display!
1369
	$cur_profile['avatar'] = $profile_vars['avatar'];
1370
1371
	return false;
1372
}
1373
1374
/**
1375
 * Save a members group.
1376
 *
1377
 * @param int $value
1378
 *
1379
 * @return bool
1380
 * @throws \ElkArte\Exceptions\Exception at_least_one_admin
1381
 */
1382
function profileSaveGroups(&$value)
1383
{
1384
	global $profile_vars, $old_profile, $context, $cur_profile;
1385
1386
	$db = database();
1387
1388
	// Do we need to protect some groups?
1389
	if (!allowedTo('admin_forum'))
1390
	{
1391
		$protected_groups = array(1);
1392
		$db->fetchQuery('
1393
			SELECT 
1394
				id_group
1395
			FROM {db_prefix}membergroups
1396
			WHERE group_type = {int:is_protected}',
1397
			array(
1398
				'is_protected' => 1,
1399
			)
1400
		)->fetch_callback(
1401
			function ($row) use (&$protected_groups) {
1402
				$protected_groups[] = (int) $row['id_group'];
1403
			}
1404
		);
1405
1406
		$protected_groups = array_unique($protected_groups);
1407
	}
1408
1409
	// The account page allows the change of your id_group - but not to a protected group!
1410
	if (empty($protected_groups) || count(array_intersect(array((int) $value, $old_profile['id_group']), $protected_groups)) === 0)
1411
	{
1412
		$value = (int) $value;
1413
	}
1414
	// ... otherwise it's the old group sir.
1415
	else
1416
	{
1417
		$value = $old_profile['id_group'];
1418
	}
1419
1420
	// Find the additional membergroups (if any)
1421
	if (isset($_POST['additional_groups']) && is_array($_POST['additional_groups']))
1422
	{
1423
		$additional_groups = array();
1424
		foreach ($_POST['additional_groups'] as $group_id)
1425
		{
1426
			$group_id = (int) $group_id;
1427
			if (!empty($group_id) && (empty($protected_groups) || !in_array($group_id, $protected_groups)))
1428
			{
1429
				$additional_groups[] = $group_id;
1430
			}
1431
		}
1432
1433
		// Put the protected groups back in there if you don't have permission to take them away.
1434
		$old_additional_groups = explode(',', $old_profile['additional_groups']);
1435
		foreach ($old_additional_groups as $group_id)
1436
		{
1437
			if (!empty($protected_groups) && in_array($group_id, $protected_groups))
1438
			{
1439
				$additional_groups[] = $group_id;
1440
			}
1441
		}
1442
1443
		if (implode(',', $additional_groups) !== $old_profile['additional_groups'])
1444
		{
1445
			$profile_vars['additional_groups'] = implode(',', $additional_groups);
1446
			$cur_profile['additional_groups'] = implode(',', $additional_groups);
1447
		}
1448
	}
1449
1450
	// Too often, people remove delete their own account, or something.
1451
	if (in_array(1, explode(',', $old_profile['additional_groups'])) || $old_profile['id_group'] == 1)
1452
	{
1453
		$stillAdmin = $value == 1 || (isset($additional_groups) && in_array(1, $additional_groups));
1454
1455
		// If they would no longer be an admin, look for any other...
1456
		if (!$stillAdmin)
1457
		{
1458
			$request = $db->query('', '
1459
				SELECT 
1460
					id_member
1461
				FROM {db_prefix}members
1462
				WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1463
					AND id_member != {int:selected_member}
1464
				LIMIT 1',
1465
				array(
1466
					'admin_group' => 1,
1467
					'selected_member' => $context['id_member'],
1468
				)
1469
			);
1470
			list ($another) = $request->fetch_row();
1471
			$request->free_result();
1472
1473
			if (empty($another))
1474
			{
1475
				throw new \ElkArte\Exceptions\Exception('at_least_one_admin', 'critical');
1476
			}
1477
		}
1478
	}
1479
1480
	// If we are changing group status, update permission cache as necessary.
1481
	if ($value != $old_profile['id_group'] || isset($profile_vars['additional_groups']))
1482
	{
1483
		if ($context['user']['is_owner'])
1484
		{
1485
			$_SESSION['mc']['time'] = 0;
1486
		}
1487
		else
1488
		{
1489
			updateSettings(array('settings_updated' => time()));
1490
		}
1491
	}
1492
1493
	return true;
1494
}
1495
1496
/**
1497
 * Get the data about a users warnings.
1498
 * Returns an array of them
1499
 *
1500
 * @param int $start The item to start with (for pagination purposes)
1501
 * @param int $items_per_page The number of items to show per page
1502
 * @param string $sort A string indicating how to sort the results
1503
 * @param int $memID the member ID
1504
 *
1505
 * @return array
1506
 */
1507
function list_getUserWarnings($start, $items_per_page, $sort, $memID)
1508
{
1509
	$db = database();
1510
1511
	$previous_warnings = array();
1512
	$db->fetchQuery('
1513
		SELECT 
1514
			COALESCE(mem.id_member, 0) AS id_member, COALESCE(mem.real_name, lc.member_name) AS member_name,
1515
			lc.log_time, lc.body, lc.counter, lc.id_notice
1516
		FROM {db_prefix}log_comments AS lc
1517
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lc.id_member)
1518
		WHERE lc.id_recipient = {int:selected_member}
1519
			AND lc.comment_type = {string:warning}
1520
		ORDER BY ' . $sort . '
1521
		LIMIT ' . $items_per_page . '  OFFSET ' . $start,
1522
		array(
1523
			'selected_member' => $memID,
1524
			'warning' => 'warning',
1525
		)
1526
	)->fetch_callback(
1527
		function ($row) use (&$previous_warnings) {
1528
			$previous_warnings[] = array(
1529
				'issuer' => array(
1530
					'id' => $row['id_member'],
1531
					'link' => $row['id_member'] ? ('<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['member_name']]) . '">' . $row['member_name'] . '</a>') : $row['member_name'],
1532
				),
1533
				'time' => standardTime($row['log_time']),
1534
				'html_time' => htmlTime($row['log_time']),
1535
				'timestamp' => forum_time(true, $row['log_time']),
1536
				'reason' => $row['body'],
1537
				'counter' => $row['counter'] > 0 ? '+' . $row['counter'] : $row['counter'],
1538
				'id_notice' => $row['id_notice'],
1539
			);
1540
		}
1541
	);
1542
1543
	return $previous_warnings;
1544
}
1545
1546
/**
1547
 * Get the number of warnings a user has.
1548
 * Returns the total number of warnings for the user
1549
 *
1550
 * @param int $memID
1551
 * @return int the number of warnings
1552
 */
1553
function list_getUserWarningCount($memID)
1554
{
1555
	$db = database();
1556
1557
	$request = $db->query('', '
1558
		SELECT 
1559
			COUNT(*)
1560
		FROM {db_prefix}log_comments
1561
		WHERE id_recipient = {int:selected_member}
1562
			AND comment_type = {string:warning}',
1563
		array(
1564
			'selected_member' => $memID,
1565
			'warning' => 'warning',
1566
		)
1567
	);
1568
	list ($total_warnings) = $request->fetch_row();
1569
	$request->free_result();
1570
1571
	return $total_warnings;
1572
}
1573
1574
/**
1575
 * Get a list of attachments for this user
1576
 * (used by createList() callback and others)
1577
 *
1578
 * @param int $start The item to start with (for pagination purposes)
1579
 * @param int $items_per_page The number of items to show per page
1580
 * @param string $sort A string indicating how to sort the results
1581
 * @param int[] $boardsAllowed
1582
 * @param int $memID
1583
 * @param int[]|null|bool $exclude_boards
1584
 *
1585
 * @return array
1586
 */
1587
function profileLoadAttachments($start, $items_per_page, $sort, $boardsAllowed, $memID, $exclude_boards = null)
1588
{
1589
	global $board, $modSettings, $context;
1590
1591
	$db = database();
1592
1593
	if ($exclude_boards === null && !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0)
1594
	{
1595
		$exclude_boards = array($modSettings['recycle_board']);
1596
	}
1597
1598
	// Retrieve some attachments.
1599
	$attachments = array();
1600
	$db->fetchQuery('
1601
		SELECT
1602
		 	a.id_attach, a.id_msg, a.filename, a.downloads, a.approved, a.fileext, a.width, a.height, ' .
1603
		(empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ' COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height, ') . '
1604
			m.id_msg, m.id_topic, m.id_board, m.poster_time, m.subject, b.name
1605
		FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
1606
			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
1607
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1608
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
1609
		WHERE a.attachment_type = {int:attachment_type}
1610
			AND a.id_msg != {int:no_message}
1611
			AND m.id_member = {int:current_member}' . (!empty($board) ? '
1612
			AND b.id_board = {int:board}' : '') . (!in_array(0, $boardsAllowed) ? '
1613
			AND b.id_board IN ({array_int:boards_list})' : '') . (!empty($exclude_boards) ? '
1614
			AND b.id_board NOT IN ({array_int:exclude_boards})' : '') . (!$modSettings['postmod_active'] || $context['user']['is_owner'] ? '' : '
1615
			AND m.approved = {int:is_approved}') . '
1616
		ORDER BY {raw:sort}
1617
		LIMIT {int:limit} OFFSET {int:offset} ',
1618
		array(
1619
			'boards_list' => $boardsAllowed,
1620
			'exclude_boards' => $exclude_boards,
1621
			'attachment_type' => 0,
1622
			'no_message' => 0,
1623
			'current_member' => $memID,
1624
			'is_approved' => 1,
1625
			'board' => $board,
1626
			'sort' => $sort,
1627
			'offset' => $start,
1628
			'limit' => $items_per_page,
1629
		)
1630
	)->fetch_callback(
1631
		function ($row) use (&$attachments) {
1632
			global $txt, $settings, $modSettings;
1633
1634
			$row['subject'] = censor($row['subject']);
1635
			if (!$row['approved'])
1636
			{
1637
				$row['filename'] = str_replace(array('{attachment_link}', '{txt_awaiting}'), array('<a href="' . getUrl('attach', ['action' => 'dlattach', 'attach' => $row['id_attach'], 'name' => $row['filename'], 'topic' => $row['id_topic'], 'subject' => $row['subject']]) . '">' . $row['filename'] . '</a>', $txt['awaiting_approval']), $settings['attachments_awaiting_approval']);
1638
			}
1639
			else
1640
			{
1641
				$row['filename'] = '<a href="' . getUrl('attach', ['action' => 'dlattach', 'attach' => $row['id_attach'], 'name' => $row['filename'], 'topic' => $row['id_topic'], 'subject' => $row['subject']]) . '">' . $row['filename'] . '</a>';
1642
			}
1643
1644
			$attachments[] = array(
1645
				'id' => $row['id_attach'],
1646
				'filename' => $row['filename'],
1647
				'fileext' => $row['fileext'],
1648
				'width' => $row['width'],
1649
				'height' => $row['height'],
1650
				'downloads' => $row['downloads'],
1651
				'is_image' => !empty($row['width']) && !empty($row['height']) && !empty($modSettings['attachmentShowImages']),
1652
				'id_thumb' => !empty($row['id_thumb']) ? $row['id_thumb'] : '',
1653
				'subject' => '<a href="' . getUrl('topic', ['topic' => $row['id_topic'], 'start' => 'msg' . $row['id_msg'], 'subject' => $row['subject']]) . '#msg' . $row['id_msg'] . '" rel="nofollow">' . censor($row['subject']) . '</a>',
1654
				'posted' => $row['poster_time'],
1655
				'msg' => $row['id_msg'],
1656
				'topic' => $row['id_topic'],
1657
				'board' => $row['id_board'],
1658
				'board_name' => $row['name'],
1659
				'approved' => $row['approved'],
1660
			);
1661
		}
1662
	);
1663
1664
	return $attachments;
1665
}
1666
1667
/**
1668
 * Gets the total number of attachments for the user
1669
 * (used by createList() callbacks)
1670
 *
1671
 * @param int[] $boardsAllowed
1672
 * @param int $memID
1673
 * @return int number of attachments
1674
 */
1675
function getNumAttachments($boardsAllowed, $memID)
1676
{
1677
	global $board, $modSettings, $context;
1678
1679
	$db = database();
1680
1681
	// Get the total number of attachments they have posted.
1682
	$request = $db->query('', '
1683
		SELECT 
1684
			COUNT(*)
1685
		FROM {db_prefix}attachments AS a
1686
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1687
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
1688
		WHERE a.attachment_type = {int:attachment_type}
1689
			AND a.id_msg != {int:no_message}
1690
			AND m.id_member = {int:current_member}' . (!empty($board) ? '
1691
			AND b.id_board = {int:board}' : '') . (!in_array(0, $boardsAllowed) ? '
1692
			AND b.id_board IN ({array_int:boards_list})' : '') . (!$modSettings['postmod_active'] || $context['user']['is_owner'] ? '' : '
1693
			AND m.approved = {int:is_approved}'),
1694
		array(
1695
			'boards_list' => $boardsAllowed,
1696
			'attachment_type' => 0,
1697
			'no_message' => 0,
1698
			'current_member' => $memID,
1699
			'is_approved' => 1,
1700
			'board' => $board,
1701
		)
1702
	);
1703
	list ($attachCount) = $request->fetch_row();
1704
	$request->free_result();
1705
1706
	return $attachCount;
1707
}
1708
1709
/**
1710
 * Get the relevant topics in the unwatched list
1711
 * (used by createList() callbacks)
1712
 *
1713
 * @param int $start The item to start with (for pagination purposes)
1714
 * @param int $items_per_page The number of items to show per page
1715
 * @param string $sort A string indicating how to sort the results
1716
 * @param int $memID
1717
 *
1718
 * @return array
1719
 */
1720
function getUnwatchedBy($start, $items_per_page, $sort, $memID)
1721
{
1722
	$db = database();
1723
1724
	// Get the list of topics we can see
1725
	$topics = array();
1726
	$db->fetchQuery('
1727
		SELECT
1728
		 	lt.id_topic
1729
		FROM {db_prefix}log_topics AS lt
1730
			LEFT JOIN {db_prefix}topics AS t ON (lt.id_topic = t.id_topic)
1731
			LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
1732
			LEFT JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)' . (in_array($sort, array('mem.real_name', 'mem.real_name DESC', 'mem.poster_time', 'mem.poster_time DESC')) ? '
1733
			LEFT JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member)' : '') . '
1734
		WHERE lt.id_member = {int:current_member}
1735
			AND lt.unwatched = 1
1736
			AND {query_see_board}
1737
		ORDER BY {raw:sort}
1738
		LIMIT {int:limit} OFFSET {int:offset} ',
1739
		array(
1740
			'current_member' => $memID,
1741
			'sort' => $sort,
1742
			'offset' => $start,
1743
			'limit' => $items_per_page,
1744
		)
1745
	)->fetch_callback(
1746
		function ($row) use (&$topics) {
1747
			$topics[] = $row['id_topic'];
1748
		}
1749
	);
1750
1751
	// Any topics found?
1752
	$topicsInfo = array();
1753
	if (!empty($topics))
1754
	{
1755
		$db->fetchQuery('
1756
			SELECT 
1757
				mf.subject, mf.poster_time as started_on, COALESCE(memf.real_name, mf.poster_name) as started_by, ml.poster_time as last_post_on, COALESCE(meml.real_name, ml.poster_name) as last_post_by, t.id_topic
1758
			FROM {db_prefix}topics AS t
1759
				INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1760
				INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1761
				LEFT JOIN {db_prefix}members AS meml ON (meml.id_member = ml.id_member)
1762
				LEFT JOIN {db_prefix}members AS memf ON (memf.id_member = mf.id_member)
1763
			WHERE t.id_topic IN ({array_int:topics})',
1764
			array(
1765
				'topics' => $topics,
1766
			)
1767
		)->fetch_callback(
1768
			function ($row) use (&$topicsInfo) {
1769
				$topicsInfo[] = $row;
1770
			}
1771
		);
1772
	}
1773
1774
	return $topicsInfo;
1775
}
1776
1777
/**
1778
 * Count the number of topics in the unwatched list
1779
 *
1780
 * @param int $memID
1781
 * @return int
1782
 */
1783
function getNumUnwatchedBy($memID)
1784
{
1785
	$db = database();
1786
1787
	// Get the total number of attachments they have posted.
1788
	$request = $db->query('', '
1789
		SELECT
1790
		 	COUNT(*)
1791
		FROM {db_prefix}log_topics AS lt
1792
		LEFT JOIN {db_prefix}topics AS t ON (lt.id_topic = t.id_topic)
1793
		LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
1794
		WHERE id_member = {int:current_member}
1795
			AND unwatched = 1
1796
			AND {query_see_board}',
1797
		array(
1798
			'current_member' => $memID,
1799
		)
1800
	);
1801
	list ($unwatchedCount) = $request->fetch_row();
1802
	$request->free_result();
1803
1804
	return $unwatchedCount;
1805
}
1806
1807
/**
1808
 * Returns the total number of posts a user has made
1809
 *
1810
 * - Counts all posts or just the posts made on a particular board
1811
 *
1812
 * @param int $memID
1813
 * @param int|null $board
1814
 * @return int
1815
 */
1816
function count_user_posts($memID, $board = null)
1817
{
1818
	global $modSettings;
1819
1820
	$db = database();
1821
1822
	$is_owner = $memID == User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1823
1824
	$request = $db->query('', '
1825
		SELECT 
1826
			COUNT(*)
1827
		FROM {db_prefix}messages AS m' . (User::$info->query_see_board === '1=1' ? '' : '
0 ignored issues
show
Bug Best Practice introduced by
The property query_see_board does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1828
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})') . '
1829
		WHERE m.id_member = {int:current_member}' . (!empty($board) ? '
1830
			AND m.id_board = {int:board}' : '') . (!$modSettings['postmod_active'] || $is_owner ? '' : '
1831
			AND m.approved = {int:is_approved}'),
1832
		array(
1833
			'current_member' => $memID,
1834
			'is_approved' => 1,
1835
			'board' => $board,
1836
		)
1837
	);
1838
	list ($msgCount) = $request->fetch_row();
1839
	$request->free_result();
1840
1841
	return $msgCount;
1842
}
1843
1844
/**
1845
 * Returns the total number of new topics a user has made
1846
 *
1847
 * - Counts all posts or just the topics made on a particular board
1848
 *
1849
 * @param int $memID
1850
 * @param int|null $board
1851
 * @return int
1852
 */
1853
function count_user_topics($memID, $board = null)
1854
{
1855
	global $modSettings;
1856
1857
	$db = database();
1858
1859
	$is_owner = $memID == User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1860
1861
	$request = $db->query('', '
1862
		SELECT 
1863
			COUNT(*)
1864
		FROM {db_prefix}topics AS t' . (User::$info->query_see_board === '1=1' ? '' : '
0 ignored issues
show
Bug Best Practice introduced by
The property query_see_board does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1865
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board AND {query_see_board})') . '
1866
		WHERE t.id_member_started = {int:current_member}' . (!empty($board) ? '
1867
			AND t.id_board = {int:board}' : '') . (!$modSettings['postmod_active'] || $is_owner ? '' : '
1868
			AND t.approved = {int:is_approved}'),
1869
		array(
1870
			'current_member' => $memID,
1871
			'is_approved' => 1,
1872
			'board' => $board,
1873
		)
1874
	);
1875
	list ($msgCount) = $request->fetch_row();
1876
	$request->free_result();
1877
1878
	return $msgCount;
1879
}
1880
1881
/**
1882
 * Gets a members minimum and maximum message id
1883
 *
1884
 * - Can limit the results to a particular board
1885
 * - Used to help limit queries by proving start/stop points
1886
 *
1887
 * @param int $memID
1888
 * @param int|null $board
1889
 *
1890
 * @return array
1891
 */
1892
function findMinMaxUserMessage($memID, $board = null)
1893
{
1894
	global $modSettings;
1895
1896
	$db = database();
1897
1898
	$is_owner = $memID == User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1899
1900
	$request = $db->query('', '
1901
		SELECT 
1902
			MIN(id_msg), MAX(id_msg)
1903
		FROM {db_prefix}messages AS m
1904
		WHERE m.id_member = {int:current_member}' . (!empty($board) ? '
1905
			AND m.id_board = {int:board}' : '') . (!$modSettings['postmod_active'] || $is_owner ? '' : '
1906
			AND m.approved = {int:is_approved}'),
1907
		array(
1908
			'current_member' => $memID,
1909
			'is_approved' => 1,
1910
			'board' => $board,
1911
		)
1912
	);
1913
	$minmax = $request->fetch_row();
1914
	$request->free_result();
1915
1916
	return empty($minmax) ? array(0, 0) : $minmax;
1917
}
1918
1919
/**
1920
 * Determines a members minimum and maximum topic id
1921
 *
1922
 * - Can limit the results to a particular board
1923
 * - Used to help limit queries by proving start/stop points
1924
 *
1925
 * @param int $memID
1926
 * @param int|null $board
1927
 *
1928
 * @return array
1929
 */
1930
function findMinMaxUserTopic($memID, $board = null)
1931
{
1932
	global $modSettings;
1933
1934
	$db = database();
1935
1936
	$is_owner = $memID == User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1937
1938
	$request = $db->query('', '
1939
		SELECT 
1940
			MIN(id_topic), MAX(id_topic)
1941
		FROM {db_prefix}topics AS t
1942
		WHERE t.id_member_started = {int:current_member}' . (!empty($board) ? '
1943
			AND t.id_board = {int:board}' : '') . (!$modSettings['postmod_active'] || $is_owner ? '' : '
1944
			AND t.approved = {int:is_approved}'),
1945
		array(
1946
			'current_member' => $memID,
1947
			'is_approved' => 1,
1948
			'board' => $board,
1949
		)
1950
	);
1951
	$minmax = $request->fetch_row();
1952
	$request->free_result();
1953
1954
	return empty($minmax) ? array(0, 0) : $minmax;
1955
}
1956
1957
/**
1958
 * Used to load all the posts of a user
1959
 *
1960
 * - Can limit to just the posts of a particular board
1961
 * - If range_limit is supplied, will check if count results were returned, if not
1962
 * will drop the limit and try again
1963
 *
1964
 * @param int $memID
1965
 * @param int $start
1966
 * @param int $count
1967
 * @param string|null $range_limit
1968
 * @param bool $reverse
1969
 * @param int|null $board
1970
 *
1971
 * @return array
1972
 */
1973
function load_user_posts($memID, $start, $count, $range_limit = '', $reverse = false, $board = null)
1974
{
1975
	global $modSettings;
1976
1977
	$db = database();
1978
1979
	$is_owner = $memID == User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1980
	$user_posts = array();
1981
1982
	// Find this user's posts. The left join on categories somehow makes this faster, weird as it looks.
1983
	for ($i = 0; $i < 2; $i++)
1984
	{
1985
		$request = $db->query('', '
1986
			SELECT
1987
				b.id_board, b.name AS bname,
1988
				c.id_cat, c.name AS cname,
1989
				m.id_topic, m.id_msg, m.body, m.smileys_enabled, m.subject, m.poster_time, m.approved,
1990
				t.id_member_started, t.id_first_msg, t.id_last_msg
1991
			FROM {db_prefix}messages AS m
1992
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1993
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
1994
				LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
1995
			WHERE m.id_member = {int:current_member}' . (!empty($board) ? '
1996
				AND b.id_board = {int:board}' : '') . (empty($range_limit) ? '' : '
1997
				AND ' . $range_limit) . '
1998
				AND {query_see_board}' . (!$modSettings['postmod_active'] || $is_owner ? '' : '
1999
				AND t.approved = {int:is_approved} AND m.approved = {int:is_approved}') . '
2000
			ORDER BY m.id_msg ' . ($reverse ? 'ASC' : 'DESC') . '
2001
			LIMIT ' . $start . ', ' . $count,
2002
			array(
2003
				'current_member' => $memID,
2004
				'is_approved' => 1,
2005
				'board' => $board,
2006
			)
2007
		);
2008
2009
		// Did we get what we wanted, if so stop looking
2010
		if ($request->num_rows() === $count || empty($range_limit))
2011
		{
2012
			break;
2013
		}
2014
		else
2015
		{
2016
			$range_limit = '';
2017
		}
2018
	}
2019
2020
	// Place them in the post array
2021
	while (($row = $request->fetch_assoc()))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $request does not seem to be defined for all execution paths leading up to this point.
Loading history...
2022
	{
2023
		$user_posts[] = $row;
2024
	}
2025
	$request->free_result();
2026
2027
	return $user_posts;
2028
}
2029
2030
/**
2031
 * Used to load all the topics of a user
2032
 *
2033
 * - Can limit to just the posts of a particular board
2034
 * - If range_limit 'guess' is supplied, will check if count results were returned, if not
2035
 * it will drop the guessed limit and try again.
2036
 *
2037
 * @param int $memID
2038
 * @param int $start
2039
 * @param int $count
2040
 * @param string $range_limit
2041
 * @param bool $reverse
2042
 * @param int|null $board
2043
 *
2044
 * @return array
2045
 */
2046
function load_user_topics($memID, $start, $count, $range_limit = '', $reverse = false, $board = null)
2047
{
2048
	global $modSettings;
2049
2050
	$db = database();
2051
2052
	$is_owner = $memID == User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2053
	$user_topics = array();
2054
2055
	// Find this user's topics.  The left join on categories somehow makes this faster, weird as it looks.
2056
	for ($i = 0; $i < 2; $i++)
2057
	{
2058
		$request = $db->query('', '
2059
			SELECT
2060
				b.id_board, b.name AS bname,
2061
				c.id_cat, c.name AS cname,
2062
				t.id_member_started, t.id_first_msg, t.id_last_msg, t.approved,
2063
				m.body, m.smileys_enabled, m.subject, m.poster_time, m.id_topic, m.id_msg
2064
			FROM {db_prefix}topics AS t
2065
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
2066
				LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
2067
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
2068
			WHERE t.id_member_started = {int:current_member}' . (!empty($board) ? '
2069
				AND t.id_board = {int:board}' : '') . (empty($range_limit) ? '' : '
2070
				AND ' . $range_limit) . '
2071
				AND {query_see_board}' . (!$modSettings['postmod_active'] || $is_owner ? '' : '
2072
				AND t.approved = {int:is_approved} AND m.approved = {int:is_approved}') . '
2073
			ORDER BY t.id_first_msg ' . ($reverse ? 'ASC' : 'DESC') . '
2074
			LIMIT ' . $start . ', ' . $count,
2075
			array(
2076
				'current_member' => $memID,
2077
				'is_approved' => 1,
2078
				'board' => $board,
2079
			)
2080
		);
2081
2082
		// Did we get what we wanted, if so stop looking
2083
		if ($request->num_rows() === $count || empty($range_limit))
2084
		{
2085
			break;
2086
		}
2087
		else
2088
		{
2089
			$range_limit = '';
2090
		}
2091
	}
2092
2093
	// Place them in the topic array
2094
	while (($row = $request->fetch_assoc()))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $request does not seem to be defined for all execution paths leading up to this point.
Loading history...
2095
	{
2096
		$user_topics[] = $row;
2097
	}
2098
	$request->free_result();
2099
2100
	return $user_topics;
2101
}
2102
2103
/**
2104
 * Loads the permissions that are given to a member group or set of groups
2105
 *
2106
 * @param int[] $curGroups
2107
 *
2108
 * @return array
2109
 */
2110
function getMemberGeneralPermissions($curGroups)
2111
{
2112
	$db = database();
2113
	Txt::load('ManagePermissions');
2114
2115
	// Get all general permissions.
2116
	$general_permission = array();
2117
	$db->fetchQuery('
2118
		SELECT 
2119
			p.permission, p.add_deny, mg.group_name, p.id_group
2120
		FROM {db_prefix}permissions AS p
2121
			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = p.id_group)
2122
		WHERE p.id_group IN ({array_int:group_list})
2123
		ORDER BY p.add_deny DESC, p.permission, mg.min_posts, CASE WHEN mg.id_group < {int:newbie_group} THEN mg.id_group ELSE 4 END, mg.group_name',
2124
		array(
2125
			'group_list' => $curGroups,
2126
			'newbie_group' => 4,
2127
		)
2128
	)->fetch_callback(
2129
		function ($row) use (&$general_permission) {
2130
			global $txt;
2131
2132
			// We don't know about this permission, it doesn't exist :P.
2133
			if (!isset($txt['permissionname_' . $row['permission']]))
2134
			{
2135
				return;
2136
			}
2137
2138
			// Permissions that end with _own or _any consist of two parts.
2139
			if (in_array(substr($row['permission'], -4), array('_own', '_any')) && isset($txt['permissionname_' . substr($row['permission'], 0, -4)]))
2140
			{
2141
				$name = $txt['permissionname_' . substr($row['permission'], 0, -4)] . ' - ' . $txt['permissionname_' . $row['permission']];
2142
			}
2143
			else
2144
			{
2145
				$name = $txt['permissionname_' . $row['permission']];
2146
			}
2147
2148
			// Add this permission if it doesn't exist yet.
2149
			if (!isset($general_permission[$row['permission']]))
2150
			{
2151
				$general_permission[$row['permission']] = array(
2152
					'id' => $row['permission'],
2153
					'groups' => array(
2154
						'allowed' => array(),
2155
						'denied' => array()
2156
					),
2157
					'name' => $name,
2158
					'is_denied' => false,
2159
					'is_global' => true,
2160
				);
2161
			}
2162
2163
			// Add the membergroup to either the denied or the allowed groups.
2164
			$general_permission[$row['permission']]['groups'][empty($row['add_deny'])
2165
				? 'denied'
2166
				: 'allowed'][] = $row['id_group'] == 0
2167
					? $txt['membergroups_members']
2168
					: $row['group_name'];
2169
2170
			// Once denied is always denied.
2171 2
			$general_permission[$row['permission']]['is_denied'] |= empty($row['add_deny']);
2172
		}
2173 2
	);
2174
2175 2
	return $general_permission;
2176 2
}
2177
2178 2
/**
2179
 * Get the permissions a member has, or group they are in has
2180 2
 * If $board is supplied will return just the permissions for that board
2181
 *
2182
 * @param int $memID
2183 2
 * @param int[] $curGroups
2184
 * @param int|null $board
2185 2
 *
2186
 * @return array
2187
 */
2188 2
function getMemberBoardPermissions($memID, $curGroups, $board = null)
2189 2
{
2190 2
	$db = database();
2191 2
	Txt::load('ManagePermissions');
2192
2193
	$board_permission = array();
2194 2
	$db->fetchQuery('
2195
		SELECT
2196 2
			bp.add_deny, bp.permission, bp.id_group, mg.group_name' . (empty($board) ? '' : ',
2197
			b.id_profile, CASE WHEN mods.id_member IS NULL THEN 0 ELSE 1 END AS is_moderator') . '
2198
		FROM {db_prefix}board_permissions AS bp' . (empty($board) ? '' : '
2199
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = {int:current_board})
2200
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})') . '
2201
			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = bp.id_group)
2202
		WHERE bp.id_profile = {raw:current_profile}
2203
			AND bp.id_group IN ({array_int:group_list}' . (empty($board) ? ')' : ', {int:moderator_group})
2204 2
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})'),
2205
		array(
2206 2
			'current_board' => $board,
2207
			'group_list' => $curGroups,
2208
			'current_member' => $memID,
2209 2
			'current_profile' => empty($board) ? '1' : 'b.id_profile',
2210
			'moderator_group' => 3,
2211
		)
2212 2
	)->fetch_callback(
2213
		function ($row) use (&$board_permission, $board) {
2214 2
			global $txt;
2215
2216
			// We don't know about this permission, it doesn't exist :P.
2217 2
			if (!isset($txt['permissionname_' . $row['permission']]))
2218
			{
2219
				return;
2220 2
			}
2221
2222
			// The name of the permission using the format 'permission name' - 'own/any topic/event/etc.'.
2223
			if (in_array(substr($row['permission'], -4), array('_own', '_any')) && isset($txt['permissionname_' . substr($row['permission'], 0, -4)]))
2224
			{
2225
				$name = $txt['permissionname_' . substr($row['permission'], 0, -4)] . ' - ' . $txt['permissionname_' . $row['permission']];
2226
			}
2227
			else
2228
			{
2229
				$name = $txt['permissionname_' . $row['permission']];
2230
			}
2231
2232
			// Create the structure for this permission.
2233
			if (!isset($board_permission[$row['permission']]))
2234
			{
2235
				$board_permission[$row['permission']] = array(
2236
					'id' => $row['permission'],
2237
					'groups' => array(
2238
						'allowed' => array(),
2239
						'denied' => array()
2240
					),
2241
					'name' => $name,
2242
					'is_denied' => false,
2243
					'is_global' => empty($board),
2244
				);
2245
			}
2246
2247
			$board_permission[$row['permission']]['groups'][empty($row['add_deny'])
2248
				? 'denied'
2249
				: 'allowed'][$row['id_group']] = $row['id_group'] == 0
2250
					? $txt['membergroups_members']
2251
					: $row['group_name'];
2252
			$board_permission[$row['permission']]['is_denied'] |= empty($row['add_deny']);
2253
		}
2254
	);
2255
2256
	return $board_permission;
2257
}
2258
2259
/**
2260
 * Retrieves (most of) the IPs used by a certain member in his messages and errors
2261
 *
2262
 * @param int $memID the id of the member
2263
 *
2264
 * @return array
2265
 */
2266
function getMembersIPs($memID)
2267
{
2268
	global $modSettings;
2269
2270
	$db = database();
2271
	$member = MembersList::get($memID);
2272
2273
	// @todo cache this
2274
	// If this is a big forum, or a large posting user, let's limit the search.
2275
	if ($modSettings['totalMessages'] > 50000 && $member->posts > 500)
0 ignored issues
show
Bug Best Practice introduced by
The property posts does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
2276
	{
2277
		$request = $db->query('', '
2278
			SELECT 
2279
				MAX(id_msg)
2280
			FROM {db_prefix}messages AS m
2281
			WHERE m.id_member = {int:current_member}',
2282
			array(
2283
				'current_member' => $memID,
2284
			)
2285
		);
2286
		list ($max_msg_member) = $request->fetch_row();
2287
		$request->free_result();
2288
2289
		// There's no point worrying ourselves with messages made yonks ago, just get recent ones!
2290
		$min_msg_member = max(0, $max_msg_member - $member->posts * 3);
2291
	}
2292
2293
	// Default to at least the ones we know about.
2294
	$ips = array(
2295
		$member->member_ip,
0 ignored issues
show
Bug Best Practice introduced by
The property member_ip does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
2296
		$member->member_ip2,
0 ignored issues
show
Bug Best Practice introduced by
The property member_ip2 does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
2297
	);
2298
2299
	// @todo cache this
2300
	// Get all IP addresses this user has used for his messages.
2301
	$db->fetchQuery('
2302
		SELECT DISTINCT poster_ip
2303
		FROM {db_prefix}messages
2304
		WHERE id_member = {int:current_member} ' . (isset($min_msg_member) ? '
2305
			AND id_msg >= {int:min_msg_member} AND id_msg <= {int:max_msg_member}' : ''),
2306
		array(
2307
			'current_member' => $memID,
2308
			'min_msg_member' => $min_msg_member ?? 0,
2309
			'max_msg_member' => $max_msg_member ?? 0,
2310
		)
2311
	)->fetch_callback(
2312
		function ($row) use (&$ips) {
2313
			$ips[] = $row['poster_ip'];
2314
		}
2315
	);
2316
2317
	// Now also get the IP addresses from the error messages.
2318
	$error_ips = array();
2319
	$db->fetchQuery('
2320
		SELECT 
2321
			COUNT(*) AS error_count, ip
2322
		FROM {db_prefix}log_errors
2323
		WHERE id_member = {int:current_member}
2324
		GROUP BY ip',
2325
		array(
2326
			'current_member' => $memID,
2327
		)
2328
	)->fetch_callback(
2329
		function ($row) use (&$error_ips) {
2330
			$error_ips[] = $row['ip'];
2331
		}
2332
	);
2333
2334
	return array('message_ips' => array_unique($ips), 'error_ips' => array_unique($error_ips));
2335
}
2336
2337
/**
2338
 * Return the details of the members using a certain range of IPs
2339
 * except the current one
2340
 *
2341
 * @param string[] $ips a list of IP addresses
2342
 * @param int $memID the id of the "current" member (maybe it could be retrieved with currentMemberID)
2343
 *
2344
 * @return array
2345
 */
2346
function getMembersInRange($ips, $memID)
2347
{
2348
	$db = database();
2349
2350
	$message_members = array();
2351
	$members_in_range = array();
2352
2353
	// Get member ID's which are in messages...
2354
	$db->fetchQuery('
2355
		SELECT DISTINCT mem.id_member
2356
		FROM {db_prefix}messages AS m
2357
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
2358
		WHERE m.poster_ip IN ({array_string:ip_list})
2359
			AND mem.id_member != {int:current_member}',
2360
		array(
2361
			'current_member' => $memID,
2362
			'ip_list' => $ips,
2363
		)
2364
	)->fetch_callback(
2365
		function ($row) use (&$message_members) {
2366
			$message_members[] = $row['id_member'];
2367
		}
2368
	);
2369
2370
	// And then get the member ID's belong to other users
2371
	$db->fetchQuery('
2372
		SELECT 
2373
			id_member
2374
		FROM {db_prefix}members
2375
		WHERE id_member != {int:current_member}
2376
			AND member_ip IN ({array_string:ip_list})',
2377
		array(
2378
			'current_member' => $memID,
2379
			'ip_list' => $ips,
2380
		)
2381
	)->fetch_callback(
2382
		function ($row) use (&$message_members) {
2383
			$message_members[] = $row['id_member'];
2384
		}
2385
	);
2386
2387
	// Once the IDs are all combined, let's clean them up
2388
	$message_members = array_unique($message_members);
2389
2390
	// And finally, fetch their names, cause of the GROUP BY doesn't like giving us that normally.
2391
	if (!empty($message_members))
2392
	{
2393
		require_once(SUBSDIR . '/Members.subs.php');
2394
2395
		// Get the latest activated member's display name.
2396
		$members_in_range = getBasicMemberData($message_members);
2397
	}
2398
2399
	return $members_in_range;
2400
}
2401
2402
/**
2403
 * Return a detailed situation of the notification methods for a certain member.
2404
 * Used in the profile page to load the defaults and validate the new
2405
 * settings.
2406
 *
2407
 * @param int $member_id the id of a member
2408
 *
2409
 * @return array
2410
 */
2411
function getMemberNotificationsProfile($member_id)
2412
{
2413
	global $modSettings, $txt;
2414
2415
	if (empty($modSettings['enabled_mentions']))
2416
	{
2417
		return [];
2418
	}
2419
2420
	require_once(SUBSDIR . '/Notification.subs.php');
2421
2422
	$notifiers = Notifications::instance()->getNotifiers();
2423
	$enabled_mentions = getEnabledNotifications();
2424
	$user_preferences = getUsersNotificationsPreferences($enabled_mentions, $member_id);
2425
	$mention_types = [];
2426
	$defaults = getConfiguredNotificationMethods('*');
2427
2428
	foreach ($enabled_mentions as $type)
2429
	{
2430
		$type_on = 1;
2431
		$data = [];
2432
		foreach ($notifiers as $key => $notifier)
2433
		{
2434
			if ((empty($user_preferences[$member_id]) || empty($user_preferences[$member_id][$type])) && empty($defaults[$type]))
2435
			{
2436
				continue;
2437
			}
2438
2439
			if (!isset($defaults[$type][$key]))
2440
			{
2441
				continue;
2442
			}
2443
2444
			$data[$key] = [
2445
				'name' => $key,
2446
				'id' => '',
2447
				'input_name' => 'notify[' . $type . '][' . $key . ']',
2448
				'text' => $txt['notify_' . $key],
2449
				'enabled' => in_array($key, $user_preferences[$member_id][$type] ?? [])
2450
			];
2451
2452
			if (empty($user_preferences[$member_id][$type]))
2453
			{
2454
				$type_on = 0;
2455
			}
2456
		}
2457
2458
		// In theory data should never be empty.
2459
		if (!empty($data))
2460
		{
2461
			$mention_types[$type] = [
2462
				'data' => $data,
2463
				'default_input_name' => 'notify[' . $type . '][status]',
2464
				'user_input_name' => 'notify[' . $type . '][user]',
2465
				'value' => $type_on
2466
			];
2467
		}
2468
	}
2469
2470
	return $mention_types;
2471
}
2472
2473
/**
2474
 * Retrieves custom field data based on the specified condition and area.
2475
 *
2476
 * @param string $where The condition to filter the custom fields.
2477
 * @param string $area The area to restrict the custom fields.
2478
 *
2479
 * @return array The custom field data matching the given condition and area.
2480
 */
2481
function getCustomFieldData($where, $area)
2482
{
2483
	$db = database();
2484
2485
	// Load all the relevant fields - and data.
2486
	// The fully-qualified name for rows is here because it's a reserved word in Mariadb
2487
	// 10.2.4+ and quoting would be different for MySQL/Mariadb and PSQL
2488
	$request = $db->query('', '
2489
		SELECT
2490
			col_name, field_name, field_desc, field_type, show_reg, field_length, field_options,
2491
			default_value, bbc, enclose, placement, mask, vieworder, {db_prefix}custom_fields.rows, cols
2492
		FROM {db_prefix}custom_fields
2493
		WHERE ' . $where . '
2494
		ORDER BY vieworder ASC',
2495
		[
2496
			'area' => $area,
2497
		]
2498
	);
2499
	$data = [];
2500
	while ($row = $request->fetch_assoc())
2501
	{
2502
		$data[] = $row;
2503
	}
2504
2505
	return $data;
2506
}
2507
2508
/**
2509
 * Checks if an email address is unique for a given member ID
2510
 *
2511
 * @param int $memID The member ID (0 for new member, otherwise existing member)
2512
 * @param string $email The email address to check for uniqueness
2513
 *
2514
 * @return bool Returns true if the email address is unique, false otherwise
2515
 */
2516
function isUniqueEmail($memID, $email) {
2517
	$db = database();
2518
2519
	// Email addresses should be and stay unique.
2520
	return $db->fetchQuery('
0 ignored issues
show
Bug Best Practice introduced by
The expression return $db->fetchQuery('...=> $email))->num_rows() returns the type integer which is incompatible with the documented return type boolean.
Loading history...
2521
		SELECT 
2522
			id_member
2523
		FROM {db_prefix}members
2524
		WHERE ' . ($memID !== 0 ? 'id_member != {int:selected_member} AND ' : '') . '
2525
			email_address = {string:email_address}
2526
		LIMIT 1',
2527
		[
2528
			'selected_member' => $memID,
2529
			'email_address' => $email,
2530
		]
2531
	)->num_rows();
2532
}