getConcernedMembers()   B
last analyzed

Complexity

Conditions 10
Paths 1

Size

Total Lines 83
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 37
nc 1
nop 3
dl 0
loc 83
ccs 0
cts 20
cp 0
crap 110
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file contains some useful functions for members and membergroups.
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 ElkArte\Cache\Cache;
18
use ElkArte\Converters\Html2Md;
19
use ElkArte\Errors\ErrorContext;
20
use ElkArte\Helper\DataValidator;
21
use ElkArte\Helper\TokenHash;
22
use ElkArte\Helper\Util;
23
use ElkArte\Languages\Txt;
24
use ElkArte\User;
25
26
/**
27
 * Delete one or more members.
28
 *
29
 * What it does:
30
 *
31
 * - Requires profile_remove_own or profile_remove_any permission for
32
 * respectively removing your own account or any account.
33
 * - Non-admins cannot delete admins.
34
 *
35
 * What id does
36
 * - Changes author of messages, topics and polls to guest authors.
37
 * - Removes all log entries concerning the deleted members, except the
38
 * error logs, ban logs and moderation logs.
39
 * - Removes these members' personal messages (only the inbox)
40
 * - Removes avatars, ban entries, theme settings, moderator positions, poll votes,
41
 * likes, mentions, notifications
42
 * - Removes custom field data associated with them
43
 * - Updates member statistics afterwards.
44
 *
45
 * @param int[]|int $users
46
 * @param bool $check_not_admin = false
47
 * @package Members
48
 */
49
function deleteMembers($users, $check_not_admin = false)
50
{
51
	global $modSettings;
52
53
	$db = database();
54
55
	// Try give us a while to sort this out...
56
	detectServer()->setTimeLimit(600);
57
58
	// Try to get some more memory.
59
	detectServer()->setMemoryLimit('128M');
60
61
	$users = !is_array($users) ? [$users] : array_unique($users);
62
63
	// Make sure there's no void user in here.
64
	$users = array_diff($users, [0]);
65
66
	// How many are they deleting?
67
	if (empty($users))
68
	{
69
		return;
70
	}
71
72
	if (count($users) === 1)
73
	{
74
		list ($user) = $users;
75
76
		if ($user == 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...
77
		{
78
			isAllowedTo('profile_remove_own');
79
		}
80
		else
81
		{
82
			isAllowedTo('profile_remove_any');
83
		}
84
	}
85
	else
86
	{
87
		foreach ($users as $k => $v)
88
		{
89
			$users[$k] = (int) $v;
90
		}
91
92
		// Deleting more than one?  You can't have more than one account...
93
		isAllowedTo('profile_remove_any');
94
	}
95
96
	// Get their names for logging purposes.
97
	$admins = [];
98
	$emails = [];
99
	$user_log_details = [];
100
	$db->fetchQuery('
101
		SELECT 
102
			id_member, member_name, email_address, CASE WHEN id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0 THEN 1 ELSE 0 END AS is_admin
103
		FROM {db_prefix}members
104
		WHERE id_member IN ({array_int:user_list})
105
		LIMIT ' . count($users),
106
		[
107
			'user_list' => $users,
108
			'admin_group' => 1,
109
		]
110
	)->fetch_callback(
111
		function ($row) use (&$admins, &$emails, &$user_log_details) {
112
			if ($row['is_admin'])
113
			{
114
				$admins[] = $row['id_member'];
115
			}
116
117
			$user_log_details[$row['id_member']] = [$row['id_member'], $row['member_name']];
118
			$emails[] = $row['email_address'];
119
		}
120
	);
121
122
	if (empty($user_log_details))
123
	{
124
		return;
125
	}
126
127
	// Make sure they aren't trying to delete administrators if they aren't one.  But don't bother checking if it's just themselves.
128
	if (!empty($admins) && ($check_not_admin || (!allowedTo('admin_forum') && (count($users) !== 1 || $users[0] != User::$info->id))))
129
	{
130
		$users = array_diff($users, $admins);
131
		foreach ($admins as $id)
132
		{
133
			unset($user_log_details[$id]);
134
		}
135
	}
136
137
	// No one left?
138
	if (empty($users))
139
	{
140
		return;
141
	}
142
143
	// Log the action - regardless of who is deleting it.
144
	$log_changes = [];
145
	foreach ($user_log_details as $user)
146
	{
147
		$log_changes[] = [
148
			'action' => 'delete_member',
149
			'log_type' => 'admin',
150
			'extra' => [
151
				'member' => $user[0],
152
				'name' => $user[1],
153
				'member_acted' => User::$info->name,
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
154
			],
155
		];
156
157
		// Remove any cached data if enabled.
158
		Cache::instance()->remove('user_settings-' . $user[0]);
159
	}
160
161
	// Make these peoples' posts guest posts.
162
	$db->query('', '
163
		UPDATE {db_prefix}messages
164
		SET 
165
			id_member = {int:guest_id}' . (!empty($modSettings['deleteMembersRemovesEmail']) ? ',
166
		poster_email = {string:blank_email}' : '') . '
167
		WHERE id_member IN ({array_int:users})',
168
		[
169
			'guest_id' => 0,
170
			'blank_email' => '',
171
			'users' => $users,
172
		]
173
	);
174
	$db->query('', '
175
		UPDATE {db_prefix}polls
176
		SET 
177
			id_member = {int:guest_id}
178
		WHERE id_member IN ({array_int:users})',
179
		[
180
			'guest_id' => 0,
181
			'users' => $users,
182
		]
183
	);
184
185
	// Make these peoples' posts guest first posts and last posts.
186
	$db->query('', '
187
		UPDATE {db_prefix}topics
188
		SET 
189
			id_member_started = {int:guest_id}
190
		WHERE id_member_started IN ({array_int:users})',
191
		[
192
			'guest_id' => 0,
193
			'users' => $users,
194
		]
195
	);
196
	$db->query('', '
197
		UPDATE {db_prefix}topics
198
		SET 
199
			id_member_updated = {int:guest_id}
200
		WHERE id_member_updated IN ({array_int:users})',
201
		[
202
			'guest_id' => 0,
203
			'users' => $users,
204
		]
205
	);
206
207
	$db->query('', '
208
		UPDATE {db_prefix}log_actions
209
		SET 
210
			id_member = {int:guest_id}
211
		WHERE id_member IN ({array_int:users})',
212
		[
213
			'guest_id' => 0,
214
			'users' => $users,
215
		]
216
	);
217
218
	$db->query('', '
219
		UPDATE {db_prefix}log_banned
220
		SET 
221
			id_member = {int:guest_id}
222
		WHERE id_member IN ({array_int:users})',
223
		[
224
			'guest_id' => 0,
225
			'users' => $users,
226
		]
227
	);
228
229
	$db->query('', '
230
		UPDATE {db_prefix}log_errors
231
		SET 
232
			id_member = {int:guest_id}
233
		WHERE id_member IN ({array_int:users})',
234
		[
235
			'guest_id' => 0,
236
			'users' => $users,
237
		]
238
	);
239
240
	// Delete the member.
241
	$db->query('', '
242
		DELETE FROM {db_prefix}members
243
		WHERE id_member IN ({array_int:users})',
244
		[
245
			'users' => $users,
246
		]
247
	);
248
249
	// Delete any likes...
250
	$db->query('', '
251
		DELETE FROM {db_prefix}message_likes
252
		WHERE id_member IN ({array_int:users})',
253
		[
254
			'users' => $users,
255
		]
256
	);
257
258
	// Delete any custom field data...
259
	$db->query('', '
260
		DELETE FROM {db_prefix}custom_fields_data
261
		WHERE id_member IN ({array_int:users})',
262
		[
263
			'users' => $users,
264
		]
265
	);
266
267
	// Delete any post by email keys...
268
	$db->query('', '
269
		DELETE FROM {db_prefix}postby_emails
270
		WHERE email_to IN ({array_string:emails})',
271
		[
272
			'emails' => $emails,
273
		]
274
	);
275
276
	// Delete the logs...
277
	$db->query('', '
278
		DELETE FROM {db_prefix}log_actions
279
		WHERE id_log = {int:log_type}
280
			AND id_member IN ({array_int:users})',
281
		[
282
			'log_type' => 2,
283
			'users' => $users,
284
		]
285
	);
286
	$db->query('', '
287
		DELETE FROM {db_prefix}log_boards
288
		WHERE id_member IN ({array_int:users})',
289
		[
290
			'users' => $users,
291
		]
292
	);
293
	$db->query('', '
294
		DELETE FROM {db_prefix}log_comments
295
		WHERE id_recipient IN ({array_int:users})
296
			AND comment_type = {string:warntpl}',
297
		[
298
			'users' => $users,
299
			'warntpl' => 'warntpl',
300
		]
301
	);
302
	$db->query('', '
303
		DELETE FROM {db_prefix}log_group_requests
304
		WHERE id_member IN ({array_int:users})',
305
		[
306
			'users' => $users,
307
		]
308
	);
309
	$db->query('', '
310
		DELETE FROM {db_prefix}log_karma
311
		WHERE id_target IN ({array_int:users})
312
			OR id_executor IN ({array_int:users})',
313
		[
314
			'users' => $users,
315
		]
316
	);
317
	$db->query('', '
318
		DELETE FROM {db_prefix}log_mark_read
319
		WHERE id_member IN ({array_int:users})',
320
		[
321
			'users' => $users,
322
		]
323
	);
324
	$db->query('', '
325
		DELETE FROM {db_prefix}log_notify
326
		WHERE id_member IN ({array_int:users})',
327
		[
328
			'users' => $users,
329
		]
330
	);
331
	$db->query('', '
332
		DELETE FROM {db_prefix}log_online
333
		WHERE id_member IN ({array_int:users})',
334
		[
335
			'users' => $users,
336
		]
337
	);
338
	$db->query('', '
339
		DELETE FROM {db_prefix}log_subscribed
340
		WHERE id_member IN ({array_int:users})',
341
		[
342
			'users' => $users,
343
		]
344
	);
345
	$db->query('', '
346
		DELETE FROM {db_prefix}log_topics
347
		WHERE id_member IN ({array_int:users})',
348
		[
349
			'users' => $users,
350
		]
351
	);
352
	$db->query('', '
353
		DELETE FROM {db_prefix}collapsed_categories
354
		WHERE id_member IN ({array_int:users})',
355
		[
356
			'users' => $users,
357
		]
358
	);
359
360
	// Make their votes appear as guest votes - at least it keeps the totals right.
361
	// @todo Consider adding back in cookie protection.
362
	$db->query('', '
363
		UPDATE {db_prefix}log_polls
364
		SET 
365
			id_member = {int:guest_id}
366
		WHERE id_member IN ({array_int:users})',
367
		[
368
			'guest_id' => 0,
369
			'users' => $users,
370
		]
371
	);
372
373
	// Remove the mentions
374
	$db->query('', '
375
		DELETE FROM {db_prefix}log_mentions
376
		WHERE id_member IN ({array_int:users})',
377
		[
378
			'users' => $users,
379
		]
380
	);
381
	// And null all those that were added by him
382
	$db->query('', '
383
		UPDATE {db_prefix}log_mentions
384
		SET 
385
			id_member_from = {int:zero}
386
		WHERE id_member_from IN ({array_int:users})',
387
		[
388
			'zero' => 0,
389
			'users' => $users,
390
		]
391
	);
392
393
	// Delete personal messages.
394
	require_once(SUBSDIR . '/PersonalMessage.subs.php');
395
	deleteMessages(null, null, $users);
396
397
	$db->query('', '
398
		UPDATE {db_prefix}personal_messages
399
		SET 
400
			id_member_from = {int:guest_id}
401
		WHERE id_member_from IN ({array_int:users})',
402
		[
403
			'guest_id' => 0,
404
			'users' => $users,
405
		]
406
	);
407
408
	// They no longer exist, so we don't know who it was sent to.
409
	$db->query('', '
410
		DELETE FROM {db_prefix}pm_recipients
411
		WHERE id_member IN ({array_int:users})',
412
		[
413
			'users' => $users,
414
		]
415
	);
416
417
	// Delete avatar.
418
	require_once(SUBSDIR . '/ManageAttachments.subs.php');
419
	removeAttachments(['id_member' => $users]);
420
421
	// It's over, no more moderation for you.
422
	$db->query('', '
423
		DELETE FROM {db_prefix}moderators
424
		WHERE id_member IN ({array_int:users})',
425
		[
426
			'users' => $users,
427
		]
428
	);
429
	$db->query('', '
430
		DELETE FROM {db_prefix}group_moderators
431
		WHERE id_member IN ({array_int:users})',
432
		[
433
			'users' => $users,
434
		]
435
	);
436
437
	// If you don't exist we can't ban you.
438
	$db->query('', '
439
		DELETE FROM {db_prefix}ban_items
440
		WHERE id_member IN ({array_int:users})',
441
		[
442
			'users' => $users,
443
		]
444
	);
445
446
	// Remove individual theme settings.
447
	$db->query('', '
448
		DELETE FROM {db_prefix}themes
449
		WHERE id_member IN ({array_int:users})',
450
		[
451
			'users' => $users,
452
		]
453
	);
454
455
	// These users are nobody's buddy nomore.
456
	$db->fetchQuery('
457
		SELECT 
458
			id_member, pm_ignore_list, buddy_list
459
		FROM {db_prefix}members
460
		WHERE FIND_IN_SET({raw:pm_ignore_list}, pm_ignore_list) != 0 OR FIND_IN_SET({raw:buddy_list}, buddy_list) != 0',
461
		[
462
			'pm_ignore_list' => implode(', pm_ignore_list) != 0 OR FIND_IN_SET(', $users),
463
			'buddy_list' => implode(', buddy_list) != 0 OR FIND_IN_SET(', $users),
464
		]
465
	)->fetch_callback(
466
		function ($row) use ($users) {
467
			updateMemberData($row['id_member'], [
468
				'pm_ignore_list' => implode(',', array_diff(explode(',', $row['pm_ignore_list']), $users)),
469
				'buddy_list' => implode(',', array_diff(explode(',', $row['buddy_list']), $users))
470
			]);
471
		}
472
	);
473
474
	// Make sure no member's birthday is still sticking in the calendar...
475
	updateSettings([
476
		'calendar_updated' => time(),
477
	]);
478
479
	// Integration rocks!
480
	call_integration_hook('integrate_delete_members', [$users]);
481
482
	updateMemberStats();
483
484
	logActions($log_changes);
485
}
486
487
/**
488
 * Registers a member to the forum.
489
 *
490
 * What it does:
491
 *
492
 * - Allows two types of interface: 'guest' and 'admin'. The first
493
 * - includes hammering protection, the latter can perform the registration silently.
494
 * - The strings used in the options array are assumed to be escaped.
495
 * - Allows to perform several checks on the input, e.g. reserved names.
496
 * - The function will adjust member statistics.
497
 * - If an error is detected will fatal error on all errors unless return_errors is true.
498
 *
499
 * @param array $regOptions
500
 * @param string $ErrorContext
501
 *
502
 * @return int the ID of the newly created member
503
 * @throws \ElkArte\Exceptions\Exception no_theme
504
 * @uses Auth.subs.php
505
 * @uses Mail.subs.php
506
 *
507
 * @package Members
508
 */
509
function registerMember(&$regOptions, $ErrorContext = 'register')
510 6
{
511
	global $scripturl, $txt;
512 6
513
	$db = database();
514 6
515
	Txt::load('Login');
516
517 6
	// We'll need some external functions.
518 6
	require_once(SUBSDIR . '/Auth.subs.php');
519
	require_once(SUBSDIR . '/Mail.subs.php');
520
521 6
	// Put any errors in here.
522
	$reg_errors = ErrorContext::context($ErrorContext, 0);
523
524 6
	$regOptions['auth_method'] = 'password';
525
526 2
	// Spaces and other odd characters are evil...
527
	$regOptions['username'] = trim(preg_replace('~[\t\n\r \x0B\0\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}]+~u', ' ', $regOptions['username']));
528
529
	// Valid emails only
530 6
	if (!DataValidator::is_valid($regOptions, ['email' => 'valid_email|required|max_length[255]'], ['email' => 'trim']))
531
	{
532
		$reg_errors->addError('bad_email');
533 6
	}
534
535
	validateUsername(0, $regOptions['username'], $ErrorContext, !empty($regOptions['check_reserved_name']));
536
537
	// Generate a validation code if it's supposed to be emailed.
538 6
	$validation_code = generateValidationCode(14);
539
540
	// Does the first password match the second?
541 6
	if ($regOptions['password'] !== $regOptions['password_check'] && $regOptions['auth_method'] === 'password')
542
	{
543
		$reg_errors->addError('passwords_dont_match');
544 6
	}
545
546
	// That's kind of easy to guess...
547
	if ($regOptions['password'] === '')
548
	{
549
		if ($regOptions['auth_method'] === 'password')
550 6
		{
551
			$reg_errors->addError('no_password');
552
		}
553
		else
554
		{
555
			$regOptions['password'] = sha1(mt_rand());
556
		}
557
	}
558
559
	// Now perform hard password validation as required.
560
	if (!empty($regOptions['check_password_strength']) && $regOptions['password'] != '')
561
	{
562
		$passwordError = validatePassword($regOptions['password'], $regOptions['username'], [$regOptions['email']]);
563 6
564
		// Password isn't legal?
565 2
		if ($passwordError !== null)
0 ignored issues
show
introduced by
The condition $passwordError !== null is always true.
Loading history...
566
		{
567
			$reg_errors->addError('profile_error_password_' . $passwordError);
568 2
		}
569
	}
570
571
	// @todo move to controller
572
	// You may not be allowed to register this email.
573
	if (!empty($regOptions['check_email_ban']))
574
	{
575
		isBannedEmail($regOptions['email'], 'cannot_register', $txt['ban_register_prohibited']);
576 6
	}
577
578 2
	// Check if the email address is in use.
579
	if (userByEmail($regOptions['email'], $regOptions['username']))
580
	{
581
		$reg_errors->addError(['email_in_use', [htmlspecialchars($regOptions['email'], ENT_COMPAT, 'UTF-8')]]);
582 6
	}
583
584
	// Perhaps someone else wants to check this user
585
	call_integration_hook('integrate_register_check', [&$regOptions, &$reg_errors]);
586
587
	// If there's any errors left return them at once!
588 6
	if ($reg_errors->hasErrors())
589
	{
590
		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...
591 6
	}
592
593
	$reservedVars = [
594
		'actual_theme_url',
595
		'actual_images_url',
596
		'base_theme_dir',
597 6
		'base_theme_url',
598
		'default_images_url',
599
		'default_theme_dir',
600
		'default_theme_url',
601
		'default_template',
602
		'images_url',
603
		'number_recent_posts',
604
		'theme_dir',
605
		'theme_id',
606
		'theme_layers',
607
		'theme_templates',
608
		'theme_url',
609
	];
610
611
	// Can't change reserved vars.
612
	if (isset($regOptions['theme_vars']) && count(array_intersect(array_keys($regOptions['theme_vars']), $reservedVars)) !== 0)
613
	{
614
		throw new \ElkArte\Exceptions\Exception('no_theme');
615
	}
616 6
617
	$tokenizer = new TokenHash();
618
619
	// @since 1.0.7 - This is necessary because validateLoginPassword
620
	// uses a pass-by-ref and would convert to hash $regOptions['password']
621 6
	// making it impossible to send the reminder email and even integrate
622
	// the registration
623
	$password = $regOptions['password'];
624
625
	// Some of these might be overwritten. (the lower ones that are in the arrays below.)
626
	$regOptions['register_vars'] = [
627 6
		'member_name' => $regOptions['username'],
628
		'email_address' => $regOptions['email'],
629
		'passwd' => validateLoginPassword($password, '', $regOptions['username'], true),
630 6
		'password_salt' => $tokenizer->generate_hash(10),
631 6
		'posts' => 0,
632 6
		'date_registered' => !empty($regOptions['time']) ? $regOptions['time'] : time(),
633 6
		'member_ip' => $regOptions['interface'] === 'admin' ? '127.0.0.1' : $regOptions['ip'],
634 6
		'member_ip2' => $regOptions['interface'] === 'admin' ? '127.0.0.1' : $regOptions['ip2'],
635 6
		'validation_code' => substr(hash('sha256', $validation_code), 0, 10),
636 6
		'real_name' => !empty($regOptions['real_name']) ? $regOptions['real_name'] : $regOptions['username'],
637 6
		'pm_email_notify' => 1,
638 6
		'id_theme' => 0,
639 6
		'id_post_group' => 4,
640 6
		'lngfile' => '',
641 6
		'buddy_list' => '',
642 6
		'pm_ignore_list' => '',
643 6
		'message_labels' => '',
644 6
		'website_title' => '',
645 6
		'website_url' => '',
646 6
		'time_format' => '',
647 6
		'signature' => '',
648 6
		'avatar' => '',
649 6
		'usertitle' => '',
650 6
		'secret_question' => '',
651 6
		'secret_answer' => '',
652 6
		'additional_groups' => '',
653 6
		'ignore_boards' => '',
654 6
		'notify_announcements' => (!empty($regOptions['notify_announcements']) ? 1 : 0),
655 6
	];
656 6
657 6
	// Setup the activation status on this new account so it is correct - firstly is it an under age account?
658 6
	if ($regOptions['require'] === 'coppa')
659 6
	{
660 6
		$regOptions['register_vars']['is_activated'] = 5;
661
		// @todo This should be changed.  To what should be it be changed??
662
		$regOptions['register_vars']['validation_code'] = '';
663
	}
664 6
	// Maybe it can be activated right away?
665
	elseif ($regOptions['require'] === 'nothing')
666
	{
667
		$regOptions['register_vars']['is_activated'] = 1;
668
	}
669
	// Maybe it must be activated by email?
670
	elseif ($regOptions['require'] === 'activation')
671 6
	{
672
		$regOptions['register_vars']['is_activated'] = 0;
673 5
	}
674
	// Otherwise it must be awaiting approval!
675
	else
676 1
	{
677
		$regOptions['register_vars']['is_activated'] = 3;
678 1
	}
679
680
	if (isset($regOptions['memberGroup']))
681
	{
682
		require_once(SUBSDIR . '/Membergroups.subs.php');
683 1
684
		// Make sure the id_group will be valid, if this is an administrator.
685
		$regOptions['register_vars']['id_group'] = $regOptions['memberGroup'] == 1 && !allowedTo('admin_forum') ? 0 : $regOptions['memberGroup'];
686 6
687
		// Check if this group is assignable.
688 5
		$unassignableGroups = getUnassignableGroups(allowedTo('admin_forum'));
689
690
		if (in_array($regOptions['register_vars']['id_group'], $unassignableGroups))
691 5
		{
692
			$regOptions['register_vars']['id_group'] = 0;
693
		}
694 5
	}
695
696 5
	// Integrate optional member settings to be set.
697
	if (!empty($regOptions['extra_register_vars']))
698
	{
699
		foreach ($regOptions['extra_register_vars'] as $var => $value)
700
		{
701
			$regOptions['register_vars'][$var] = $value;
702
		}
703 6
	}
704
705
	// Integrate optional user theme options to be set.
706
	$theme_vars = [];
707
	if (!empty($regOptions['theme_vars']))
708
	{
709
		foreach ($regOptions['theme_vars'] as $var => $value)
710
		{
711
			$theme_vars[$var] = $value;
712 6
		}
713 6
	}
714
715
	// Right, now let's prepare for insertion.
716
	$knownInts = [
717
		'date_registered', 'posts', 'id_group', 'last_login', 'personal_messages', 'unread_messages', 'notifications',
718
		'new_pm', 'pm_prefs', 'show_online', 'pm_email_notify', 'karma_good', 'karma_bad',
719
		'notify_announcements', 'notify_send_body', 'notify_regularity', 'notify_types', 'notify_from',
720
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
721
	];
722
	$knownFloats = [
723 6
		'time_offset',
724
	];
725
726
	// Call an optional function to validate the users' input.
727
	call_integration_hook('integrate_register', [&$regOptions, &$theme_vars, &$knownInts, &$knownFloats]);
728
729 6
	$column_names = [];
730
	$values = [];
731
	foreach ($regOptions['register_vars'] as $var => $val)
732
	{
733 6
		$type = 'string';
734
		if (in_array($var, $knownInts))
735 6
		{
736 6
			$type = 'int';
737 6
		}
738
		elseif (in_array($var, $knownFloats))
739 6
		{
740 6
			$type = 'float';
741
		}
742 6
		elseif ($var === 'birthdate')
743
		{
744 6
			$type = 'date';
745
		}
746
747
		$column_names[$var] = $type;
748 6
		$values[$var] = $val;
749
	}
750
751
	// Register them into the database.
752
	$db->insert('',
753 6
		'{db_prefix}members',
754 6
		$column_names,
755
		$values,
756
		['id_member']
757
	);
758 6
	$memberID = $db->insert_id('{db_prefix}members');
759 6
760 2
	// Update the number of members and latest member's info - and pass the name, but remove the 's.
761 2
	if ($regOptions['register_vars']['is_activated'] == 1)
762 6
	{
763
		updateMemberStats($memberID, $regOptions['register_vars']['real_name']);
0 ignored issues
show
Bug introduced by
It seems like $memberID can also be of type boolean; however, parameter $id_member of updateMemberStats() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

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

763
		updateMemberStats(/** @scrutinizer ignore-type */ $memberID, $regOptions['register_vars']['real_name']);
Loading history...
764 6
	}
765
	else
766
	{
767 6
		updateMemberStats();
768
	}
769 5
770
	// @todo there's got to be a method that does this
771
	// Theme variables too?
772
	if (!empty($theme_vars))
773 1
	{
774
		$inserts = [];
775
		foreach ($theme_vars as $var => $val)
776
		{
777
			$inserts[] = [$memberID, $var, $val];
778 6
		}
779
		$db->insert('insert',
780
			'{db_prefix}themes',
781
			['id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'],
782
			$inserts,
783
			['id_member', 'variable']
784
		);
785
	}
786
787
	// If it's enabled, increase the registrations for today.
788
	trackStats(['registers' => '+']);
789
790
	// @todo emails should be sent from the controller, with a new method.
791
792
	// Don't worry about what the emails might want to replace. Just give them everything and let them sort it out.
793
	$replacements = [
794 6
		'REALNAME' => $regOptions['register_vars']['real_name'],
795
		'USERNAME' => $regOptions['username'],
796
		'PASSWORD' => $regOptions['password'],
797
		'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder',
798
		'ACTIVATIONLINK' => $scripturl . '?action=register;sa=activate;u=' . $memberID . ';code=' . $validation_code,
799
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=register;sa=activate;u=' . $memberID,
800 6
		'ACTIVATIONCODE' => $validation_code,
801 6
		'COPPALINK' => $scripturl . '?action=about;sa=coppa;u=' . $memberID,
802 6
	];
803 6
804 6
	// Administrative registrations are a bit different...
805 6
	if ($regOptions['interface'] === 'admin')
806 6
	{
807 6
		if ($regOptions['require'] === 'activation')
808 6
		{
809
			$email_message = 'admin_register_activate';
810
		}
811
		elseif (!empty($regOptions['send_welcome_email']))
812 6
		{
813
			$email_message = 'admin_register_immediate';
814 4
		}
815
816 1
		if (isset($email_message))
817
		{
818 4
			$emaildata = loadEmailTemplate($email_message, $replacements);
819
820
			sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
821
		}
822
	}
823 4
	else
824
	{
825 1
		// Can post straight away - welcome them to your fantastic community...
826
		if ($regOptions['require'] == 'nothing')
827 4
		{
828
			if (!empty($regOptions['send_welcome_email']))
829
			{
830
				$replacements = [
831
					'REALNAME' => $regOptions['register_vars']['real_name'],
832
					'USERNAME' => $regOptions['username'],
833 2
					'PASSWORD' => $regOptions['password'],
834
					'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder',
835 2
				];
836
				$emaildata = loadEmailTemplate('register_immediate', $replacements);
837
				$mark_down = new Html2Md(str_replace("\n", '<br>', $emaildata['body']));
838
				$emaildata['body'] = $mark_down->get_markdown();
839
840
				sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
841
			}
842
843
			// Send admin their notification.
844
			require_once(SUBSDIR . '/Notification.subs.php');
845
			sendAdminNotifications('standard', $memberID, $regOptions['username']);
0 ignored issues
show
Bug introduced by
It seems like $memberID can also be of type boolean; however, parameter $memberID of sendAdminNotifications() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

845
			sendAdminNotifications('standard', /** @scrutinizer ignore-type */ $memberID, $regOptions['username']);
Loading history...
846
		}
847
		// Need to activate their account - or fall under COPPA.
848
		elseif ($regOptions['require'] == 'activation' || $regOptions['require'] == 'coppa')
849
		{
850
			$emaildata = loadEmailTemplate('register_' . ($regOptions['require'] == 'activation' ? 'activate' : 'coppa'), $replacements);
851
			$mark_down = new Html2Md(str_replace("\n", '<br>', $emaildata['body']));
852 2
			$emaildata['body'] = $mark_down->get_markdown();
853 2
854
			sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
855
		}
856
		// Must be awaiting approval.
857
		else
858
		{
859
			$replacements = [
860
				'REALNAME' => $regOptions['register_vars']['real_name'],
861
				'USERNAME' => $regOptions['username'],
862
				'PASSWORD' => $regOptions['password'],
863
				'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder',
864
			];
865
866
			$emaildata = loadEmailTemplate('register_pending', $replacements);
867
			$mark_down = new Html2Md(str_replace("\n", '<br>', $emaildata['body']));
868
			$emaildata['body'] = $mark_down->get_markdown();
869
870
			sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
871
872
			// Admin gets informed here...
873
			require_once(SUBSDIR . '/Notification.subs.php');
874
			sendAdminNotifications('approval', $memberID, $regOptions['username']);
875
		}
876
877
		// Okay, they're for sure registered... make sure the session is aware of this for security. (Just married :P!)
878
		$_SESSION['just_registered'] = 1;
879
	}
880
881
	// If they are for sure registered, let other people to know about it
882
	call_integration_hook('integrate_register_after', [$regOptions, $memberID]);
883
884
	return $memberID;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $memberID also could return the type boolean which is incompatible with the documented return type integer.
Loading history...
885
}
886
887
/**
888 2
 * Check if a name is in the reserved words list. (name, current member id, name/username?.)
889
 *
890
 * - checks if name is a reserved name or username.
891
 * - if is_name is false, the name is assumed to be a username.
892 6
 * - the id_member variable is used to ignore duplicate matches with the current member.
893
 *
894 6
 * @param string $name
895
 * @param int $current_ID_MEMBER
896
 * @param bool $is_name
897
 * @param bool $fatal
898
 *
899
 * @return bool
900
 * @throws \ElkArte\Exceptions\Exception username_reserved, name_censored
901
 * @package Members
902
 *
903
 */
904
function isReservedName($name, $current_ID_MEMBER = 0, $is_name = true, $fatal = true)
905
{
906
	global $modSettings;
907
908
	$db = database();
909
910
	$name = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'replaceEntities__callback', $name);
911
	$checkName = Util::strtolower($name);
912
913
	// Administrators are never restricted ;).
914
	if (!allowedTo('admin_forum') && ((!empty($modSettings['reserveName']) && $is_name) || !empty($modSettings['reserveUser']) && !$is_name))
915
	{
916 2
		$reservedNames = explode("\n", $modSettings['reserveNames']);
917
918 2
		// Case sensitive check?
919
		$checkMe = empty($modSettings['reserveCase']) ? $checkName : $name;
920 2
921 2
		// Check each name in the list...
922
		foreach ($reservedNames as $reserved)
923
		{
924 2
			if ($reserved === '')
925
			{
926
				continue;
927
			}
928
929
			// The admin might've used entities too, level the playing field.
930
			$reservedCheck = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'replaceEntities__callback', $reserved);
931
932
			// Case sensitive name?
933
			if (empty($modSettings['reserveCase']))
934
			{
935
				$reservedCheck = Util::strtolower($reservedCheck);
936
			}
937
938
			// If it's not just entire word, check for it in there somewhere...
939
			if ($checkMe === $reservedCheck || (Util::strpos($checkMe, $reservedCheck) !== false && empty($modSettings['reserveWord'])))
940
			{
941
				if ($fatal)
942
				{
943
					throw new \ElkArte\Exceptions\Exception('username_reserved', 'password', [$reserved]);
944
				}
945
946
				return true;
947
			}
948
		}
949
950
		$censor_name = $name;
951
		if (censor($censor_name) != $name)
952
		{
953
			if ($fatal)
954
			{
955
				throw new \ElkArte\Exceptions\Exception('name_censored', 'password', [$name]);
956
			}
957
958
			return true;
959
		}
960
	}
961
962
	// Characters we just shouldn't allow, regardless.
963
	foreach (['*'] as $char)
964
	{
965
		if (str_contains($checkName, $char))
966
		{
967
			if ($fatal)
968
			{
969
				throw new \ElkArte\Exceptions\Exception('username_reserved', 'password', [$char]);
970
			}
971
972
			return true;
973 2
		}
974
	}
975 2
976
	// Get rid of any SQL parts of the reserved name...
977
	$checkName = strtr($name, ['_' => '\\_', '%' => '\\%']);
978
979
	// Make sure they don't want someone else's name.
980
	$request = $db->query('', '
981
		SELECT 
982 1
			id_member
983
		FROM {db_prefix}members
984
		WHERE ' . (empty($current_ID_MEMBER) ? '' : 'id_member != {int:current_member}
985
			AND ') . '({column_case_insensitive:real_name} LIKE {string_case_insensitive:check_name} OR {column_case_insensitive:member_name} LIKE {string:check_name})
986
		LIMIT 1',
987 2
		[
988
			'current_member' => $current_ID_MEMBER,
989
			'check_name' => $checkName,
990 2
		]
991
	);
992
	if ($request->num_rows() > 0)
993
	{
994 2
		$request->free_result();
995 2
996
		return true;
997
	}
998 2
999 2
	// Does name case insensitive match a member group name?
1000
	$request = $db->query('', '
1001
		SELECT 
1002 2
			id_group
1003
		FROM {db_prefix}membergroups
1004
		WHERE {column_case_insensitive:group_name} LIKE {string_case_insensitive:check_name}
1005
		LIMIT 1',
1006
		[
1007
			'check_name' => $checkName,
1008
		]
1009
	);
1010 2
	if ($request->num_rows() > 0)
1011
	{
1012
		$request->free_result();
1013
1014
		return true;
1015
	}
1016
1017 2
	// Okay, they passed.
1018
	return false;
1019
}
1020 2
1021
/**
1022
 * Retrieves a list of membergroups that are allowed to do the given
1023
 * permission. (on the given board)
1024
 *
1025
 * - If board_id is not null, a board permission is assumed.
1026
 * - The function takes different permission settings into account.
1027
 *
1028 2
 * @param string $permission
1029
 * @param int|null $board_id = null
1030
 *
1031
 * @return array containing an array for the allowed membergroup ID's
1032
 * and an array for the denied membergroup ID's.
1033
 * @throws \ElkArte\Exceptions\Exception no_board
1034
 * @package Members
1035
 *
1036
 */
1037
function groupsAllowedTo($permission, $board_id = null)
1038
{
1039
	global $board_info;
1040
1041
	$db = database();
1042
1043
	// Admins are allowed to do anything.
1044
	$member_groups = [
1045
		'allowed' => [1],
1046
		'denied' => [],
1047
	];
1048
1049 2
	// Assume we're dealing with regular permissions (like profile_view_own).
1050
	if ($board_id === null)
1051 2
	{
1052
		$db->fetchQuery('
1053
			SELECT 
1054
				id_group, add_deny
1055 2
			FROM {db_prefix}permissions
1056
			WHERE permission = {string:permission}',
1057
			[
1058
				'permission' => $permission,
1059
			]
1060 2
		)->fetch_callback(
1061
			function ($row) use (&$member_groups) {
1062
				$member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = (int) $row['id_group'];
1063
			}
1064
		);
1065
	}
1066
	// Otherwise it's time to look at the board.
1067
	else
1068
	{
1069
		// First get the profile of the given board.
1070
		if (isset($board_info['id']) && (int) $board_info['id'] === (int) $board_id)
1071
		{
1072
			$profile_id = (int) $board_info['profile'];
1073
		}
1074
		elseif ($board_id !== 0)
1075
		{
1076
			require_once(SUBSDIR . '/Boards.subs.php');
1077
			$board_data = fetchBoardsInfo(['boards' => $board_id], ['selects' => 'permissions']);
1078
1079
			if (empty($board_data))
1080 2
			{
1081
				throw new \ElkArte\Exceptions\Exception('no_board');
1082
			}
1083
			$profile_id = (int) $board_data[$board_id]['id_profile'];
1084 2
		}
1085
		else
1086
		{
1087
			$profile_id = 1;
1088
		}
1089
1090
		$db->fetchQuery('
1091
			SELECT 
1092
				bp.id_group, bp.add_deny
1093
			FROM {db_prefix}board_permissions AS bp
1094
			WHERE bp.permission = {string:permission}
1095
				AND bp.id_profile = {int:profile_id}',
1096
			[
1097 2
				'profile_id' => $profile_id,
1098
				'permission' => $permission,
1099
			]
1100 2
		)->fetch_callback(
1101
			function ($row) use (&$member_groups) {
1102
				$member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = (int) $row['id_group'];
1103
			}
1104
		);
1105
	}
1106
1107 2
	// Denied is never allowed.
1108 2
	$member_groups['allowed'] = array_diff($member_groups['allowed'], $member_groups['denied']);
1109
1110 2
	return $member_groups;
1111
}
1112 2
1113 2
/**
1114
 * Retrieves a list of members that have a given permission (on a given board).
1115
 *
1116
 * - If board_id is not null, a board permission is assumed.
1117
 * - Takes different permission settings into account.
1118 2
 * - Takes possible moderators (on board 'board_id') into account.
1119
 *
1120 2
 * @param string $permission
1121
 * @param int|null $board_id = null
1122
 *
1123
 * @return int[] an array containing member ID's.
1124
 * @package Members
1125
 */
1126
function membersAllowedTo($permission, $board_id = null)
1127
{
1128
	$db = database();
1129
1130
	$member_groups = groupsAllowedTo($permission, $board_id);
1131
1132
	$include_moderators = in_array(3, $member_groups['allowed']) && $board_id !== null;
1133
	$member_groups['allowed'] = array_diff($member_groups['allowed'], [3]);
1134
1135
	$exclude_moderators = in_array(3, $member_groups['denied']) && $board_id !== null;
1136
	$member_groups['denied'] = array_diff($member_groups['denied'], [3]);
1137
1138
	return $db->fetchQuery('
1139 2
		SELECT 
1140
			mem.id_member
1141 2
		FROM {db_prefix}members AS mem' . ($include_moderators || $exclude_moderators ? '
1142
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_member = mem.id_member AND mods.id_board = {int:board_id})' : '') . '
1143 2
		WHERE (' . ($include_moderators ? 'mods.id_member IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_allowed}) OR FIND_IN_SET({raw:member_group_allowed_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_allowed}))' . (empty($member_groups['denied']) ? '' : '
1144 2
			AND NOT (' . ($exclude_moderators ? 'mods.id_member IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_denied}) OR FIND_IN_SET({raw:member_group_denied_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_denied}))'),
1145
		[
1146 2
			'member_groups_allowed' => $member_groups['allowed'],
1147 2
			'member_groups_denied' => $member_groups['denied'],
1148
			'board_id' => $board_id,
1149 2
			'member_group_allowed_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['allowed']),
1150
			'member_group_denied_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['denied']),
1151
		]
1152 2
	)->fetch_callback(
1153 2
		function ($row) {
1154 2
			return (int) $row['id_member'];
1155 2
		}
1156
	);
1157 2
}
1158 2
1159 2
/**
1160 2
 * This function is used to re-associate members with relevant posts.
1161 2
 *
1162
 * - Re-attribute guest posts to a specified member.
1163 2
 * - Does not check for any permissions.
1164
 * - If add_to_post_count is set, the member's post count is increased.
1165 2
 *
1166 2
 * @param int $memID
1167
 * @param bool|false|string $email = false
1168
 * @param bool|false|string $membername = false
1169
 * @param bool $post_count = false
1170
 * @package Members
1171
 *
1172
 */
1173
function reattributePosts($memID, $email = false, $membername = false, $post_count = false)
1174
{
1175
	$db = database();
1176
1177
	// Firstly, if email and username aren't passed find out the members email address and name.
1178
	if ($email === false && $membername === false)
1179
	{
1180
		require_once(SUBSDIR . '/Members.subs.php');
1181
		$result = getBasicMemberData($memID);
1182
		$email = $result['email_address'];
1183
		$membername = $result['member_name'];
1184
	}
1185
1186
	// If they want the post count restored then we need to do some research.
1187
	if ($post_count)
1188
	{
1189
		$request = $db->query('', '
1190
			SELECT 
1191
				COUNT(*)
1192
			FROM {db_prefix}messages AS m
1193
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND b.count_posts = {int:count_posts})
1194
			WHERE m.id_member = {int:guest_id}
1195
				AND m.approved = {int:is_approved}
1196
				AND m.icon != {string:recycled_icon}' . (empty($email) ? '' : '
1197
				AND m.poster_email = {string:email_address}') . (empty($membername) ? '' : '
1198
				AND m.poster_name = {string:member_name}'),
1199
			[
1200
				'count_posts' => 0,
1201
				'guest_id' => 0,
1202
				'email_address' => $email,
1203
				'member_name' => $membername,
1204
				'is_approved' => 1,
1205
				'recycled_icon' => 'recycled',
1206
			]
1207
		);
1208
		list ($messageCount) = $request->fetch_row();
1209
		$request->free_result();
1210
1211
		updateMemberData($memID, ['posts' => 'posts + ' . $messageCount]);
1212
	}
1213
1214
	$query_parts = [];
1215
	if (!empty($email))
1216
	{
1217
		$query_parts[] = 'poster_email = {string:email_address}';
1218
	}
1219
1220
	if (!empty($membername))
1221
	{
1222
		$query_parts[] = 'poster_name = {string:member_name}';
1223
	}
1224
1225
	$query = implode(' AND ', $query_parts);
1226
1227
	// Finally, update the posts themselves!
1228
	$db->query('', '
1229
		UPDATE {db_prefix}messages
1230
		SET 
1231
			id_member = {int:memID}
1232
		WHERE ' . $query,
1233
		[
1234
			'memID' => $memID,
1235
			'email_address' => $email,
1236
			'member_name' => $membername,
1237
		]
1238
	);
1239
1240
	// ...and the topics too!
1241
	$db->query('', '
1242
		UPDATE {db_prefix}topics as t, {db_prefix}messages as m
1243
		SET 
1244
			t.id_member_started = {int:memID}
1245
		WHERE m.id_member = {int:memID}
1246
			AND t.id_first_msg = m.id_msg',
1247
		[
1248
			'memID' => $memID,
1249
		]
1250
	);
1251
1252
	// Allow mods with their own post tables to re-attribute posts as well :)
1253
	call_integration_hook('integrate_reattribute_posts', [$memID, $email, $membername, $post_count]);
1254
}
1255
1256
/**
1257
 * Gets a listing of members, Callback for createList().
1258
 *
1259
 * @param int $start The item to start with (for pagination purposes)
1260
 * @param int $items_per_page The number of items to show per page
1261
 * @param string $sort A string indicating how to sort the results
1262
 * @param string $where
1263
 * @param array $where_params
1264
 * @param bool $get_duplicates
1265
 *
1266
 * @return array
1267
 * @package Members
1268
 *
1269
 */
1270
function list_getMembers($start, $items_per_page, $sort, $where, $where_params = [], $get_duplicates = false)
1271
{
1272
	$db = database();
1273
1274
	$members = $db->fetchQuery('
1275
		SELECT
1276
			mem.id_member, mem.member_name, mem.real_name, mem.email_address, mem.member_ip, mem.member_ip2, mem.last_login,
1277
			mem.posts, mem.is_activated, mem.date_registered, mem.id_group, mem.additional_groups, mg.group_name
1278
		FROM {db_prefix}members AS mem
1279
			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group)
1280
		WHERE ' . ($where == '1' ? '1=1' : $where) . '
1281
		ORDER BY {raw:sort}
1282
		LIMIT {int:per_page} OFFSET {int:start}',
1283
		array_merge($where_params, [
1284
			'sort' => $sort,
1285
			'start' => $start,
1286
			'per_page' => $items_per_page,
1287
		])
1288
	)->fetch_all();
1289
1290
	// If we want duplicates pass the members array off.
1291
	if ($get_duplicates)
1292
	{
1293
		populateDuplicateMembers($members);
1294
	}
1295
1296
	return $members;
1297
}
1298
1299
/**
1300
 * Gets the number of members, Callback for createList().
1301
 *
1302
 * @param string $where
1303
 * @param array $where_params
1304
 *
1305
 * @return int
1306
 * @package Members
1307
 *
1308
 */
1309
function list_getNumMembers($where, $where_params = [])
1310
{
1311
	global $modSettings;
1312
1313
	$db = database();
1314
1315
	// We know how many members there are in total.
1316
	if (empty($where) || $where == '1=1')
1317
	{
1318
		$num_members = $modSettings['totalMembers'];
1319
	}
1320
1321
	// The database knows the amount when there are extra conditions.
1322
	else
1323
	{
1324
		$request = $db->query('', '
1325
			SELECT COUNT(*)
1326
			FROM {db_prefix}members AS mem
1327
			WHERE ' . $where,
1328
			array_merge($where_params, [])
1329
		);
1330
		list ($num_members) = $request->fetch_row();
1331
		$request->free_result();
1332
	}
1333
1334
	return $num_members;
1335
}
1336
1337
/**
1338
 * Find potential duplicate registration members based on the same IP address
1339
 *
1340
 * @param array $members
1341
 *
1342
 * @return bool
1343
 * @package Members
1344
 *
1345
 */
1346
function populateDuplicateMembers(&$members)
1347
{
1348
	$db = database();
1349
1350
	// This will hold all the ip addresses.
1351
	$ips = [];
1352
	foreach ($members as $key => $member)
1353
	{
1354
		// Create the duplicate_members element.
1355
		$members[$key]['duplicate_members'] = [];
1356
1357
		// Store the IPs.
1358
		if (!empty($member['member_ip']))
1359
		{
1360
			$ips[] = $member['member_ip'];
1361
		}
1362
1363
		if (!empty($member['member_ip2']))
1364
		{
1365
			$ips[] = $member['member_ip2'];
1366
		}
1367
	}
1368
1369
	$ips = array_unique($ips);
1370
1371
	if (empty($ips))
1372
	{
1373
		return false;
1374
	}
1375
1376
	// Fetch all members with this IP address, we'll filter out the current ones in a sec.
1377
	$potential_dupes = membersByIP($ips, 'exact', true);
1378
1379
	$duplicate_members = [];
1380
	$duplicate_ids = [];
1381
	foreach ($potential_dupes as $row)
1382
	{
1383
		//$duplicate_ids[] = $row['id_member'];
1384
1385
		$member_context = [
1386
			'id' => $row['id_member'],
1387
			'name' => $row['member_name'],
1388
			'email' => $row['email_address'],
1389
			'is_banned' => $row['is_activated'] > 10,
1390
			'ip' => $row['member_ip'],
1391
			'ip2' => $row['member_ip2'],
1392
		];
1393
1394
		if (in_array($row['member_ip'], $ips))
1395
		{
1396
			$duplicate_members[$row['member_ip']][] = $member_context;
1397
		}
1398
		if ($row['member_ip'] !== $row['member_ip2'] && in_array($row['member_ip2'], $ips))
1399
		{
1400
			$duplicate_members[$row['member_ip2']][] = $member_context;
1401
		}
1402
	}
1403
1404
	// Also try to get a list of messages using these ips.
1405
	$had_ips = [];
1406
	$db->fetchQuery('
1407
		SELECT
1408
			m.poster_ip, mem.id_member, mem.member_name, mem.email_address, mem.is_activated
1409
		FROM {db_prefix}messages AS m
1410
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1411
		WHERE m.id_member != 0
1412
			' . (!empty($duplicate_ids) ? 'AND m.id_member NOT IN ({array_int:duplicate_ids})' : '') . '
1413
			AND m.poster_ip IN ({array_string:ips})',
1414
		[
1415
			'duplicate_ids' => $duplicate_ids,
1416
			'ips' => $ips,
1417
		]
1418
	)->fetch_callback(
1419
		function ($row) use (&$had_ips, &$duplicate_members) {
1420
			// Don't collect lots of the same.
1421
			if (isset($had_ips[$row['poster_ip']]) && in_array($row['id_member'], $had_ips[$row['poster_ip']]))
1422
			{
1423
				return;
1424
			}
1425
			$had_ips[$row['poster_ip']][] = $row['id_member'];
1426
1427
			$duplicate_members[$row['poster_ip']][] = [
1428
				'id' => $row['id_member'],
1429
				'name' => $row['member_name'],
1430
				'email' => $row['email_address'],
1431
				'is_banned' => $row['is_activated'] > 10,
1432
				'ip' => $row['poster_ip'],
1433
				'ip2' => $row['poster_ip'],
1434
			];
1435
		}
1436
	);
1437
1438
	// Now we have all the duplicate members, stick them with their respective member in the list.
1439
	if (!empty($duplicate_members))
1440
	{
1441
		foreach ($members as $key => $member)
1442
		{
1443
			if (isset($duplicate_members[$member['member_ip']]))
1444
			{
1445
				$members[$key]['duplicate_members'] = $duplicate_members[$member['member_ip']];
1446
			}
1447
1448
			if ($member['member_ip'] !== $member['member_ip2'] && isset($duplicate_members[$member['member_ip2']]))
1449
			{
1450
				$members[$key]['duplicate_members'] = array_merge($member['duplicate_members'], $duplicate_members[$member['member_ip2']]);
1451
			}
1452
1453
			// Check we don't have lots of the same member.
1454
			$member_track = [$member['id_member']];
1455
			foreach ($members[$key]['duplicate_members'] as $duplicate_id_member => $duplicate_member)
1456
			{
1457
				if (in_array($duplicate_member['id'], $member_track))
1458
				{
1459
					unset($members[$key]['duplicate_members'][$duplicate_id_member]);
1460
					continue;
1461
				}
1462
1463
				$member_track[] = $duplicate_member['id'];
1464
			}
1465
		}
1466
	}
1467
}
1468
1469
/**
1470
 * Find members with a given IP (first, second, exact or "relaxed")
1471
 *
1472
 * @param string|string[] $ip1 An IP or an array of IPs
1473
 * @param string $match (optional, default 'exact') if the match should be exact
1474
 *                of "relaxed" (using LIKE)
1475
 * @param bool $ip2 (optional, default false) If the query should check IP2 as well
1476
 *
1477
 * @return array
1478
 * @package Members
1479
 *
1480
 */
1481
function membersByIP($ip1, $match = 'exact', $ip2 = false)
1482
{
1483
	$db = database();
1484
1485
	$ip_params = ['ips' => []];
1486
	$ip_query = [];
1487
	foreach ([$ip1, $ip2] as $id => $ip)
1488
	{
1489
		if ($ip === false)
1490
		{
1491
			continue;
1492
		}
1493
1494
		if ($match === 'exact')
1495
		{
1496
			$ip_params['ips'] = array_merge($ip_params['ips'], (array) $ip);
1497
		}
1498
		else
1499 2
		{
1500
			$ip = (array) $ip;
1501 2
			foreach ($ip as $id_var => $ip_var)
1502 2
			{
1503 2
				$ip_var = str_replace('*', '%', $ip_var);
1504
				$ip_query[] = !str_contains($ip_var, '%') ? '= {string:ip_address_' . $id . '_' . $id_var . '}' : 'LIKE {string:ip_address_' . $id . '_' . $id_var . '}';
1505 2
				$ip_params['ip_address_' . $id . '_' . $id_var] = $ip_var;
1506
			}
1507 2
		}
1508
	}
1509
1510 2
	if ($match === 'exact')
1511
	{
1512 2
		$where = 'member_ip IN ({array_string:ips})';
1513
		if ($ip2 !== false)
1514
		{
1515
			$where .= '
1516 2
			OR member_ip2 IN ({array_string:ips})';
1517 2
		}
1518
	}
1519 2
	else
1520 2
	{
1521 2
		$where = 'member_ip ' . implode(' OR member_ip', $ip_query);
1522
		if ($ip2 !== false)
1523
		{
1524
			$where .= '
1525
			OR member_ip2 ' . implode(' OR member_ip', $ip_query);
1526 2
		}
1527
	}
1528 2
1529 2
	return $db->fetchQuery('
1530
		SELECT
1531 2
			id_member, member_name, email_address, member_ip, member_ip2, is_activated
1532
		FROM {db_prefix}members
1533
		WHERE ' . $where,
1534
		$ip_params
1535
	)->fetch_all();
1536
}
1537 2
1538 2
/**
1539
 * Find out if there is another admin than the given user.
1540
 *
1541
 * @param int $memberID ID of the member, to compare with.
1542
 *
1543
 * @return int
1544
 * @package Members
1545 2
 *
1546
 */
1547
function isAnotherAdmin($memberID)
1548
{
1549 2
	$db = database();
1550 1
1551 2
	$request = $db->query('', '
1552
		SELECT 
1553
			id_member
1554
		FROM {db_prefix}members
1555
		WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1556
			AND id_member != {int:selected_member}
1557
		LIMIT 1',
1558
		[
1559
			'admin_group' => 1,
1560
			'selected_member' => $memberID,
1561
		]
1562
	);
1563
	list ($another) = $request->fetch_row();
1564
	$request->free_result();
1565
1566
	return $another;
1567
}
1568
1569
/**
1570
 * This function retrieves a list of member ids based on a set of conditions
1571
 *
1572
 * @param array|string $query see prepareMembersByQuery
1573
 * @param array $query_params see prepareMembersByQuery
1574
 * @param bool $details if true returns additional member details (name, email, ip, etc.)
1575
 *             false will only return an array of member id's that match the conditions
1576
 * @param bool $only_active see prepareMembersByQuery
1577
 *
1578
 * @return array
1579
 * @package Members
1580
 *
1581
 */
1582
function membersBy($query, $query_params, $details = false, $only_active = true)
1583
{
1584
	$db = database();
1585
1586
	$query_where = prepareMembersByQuery($query, $query_params, $only_active);
1587
1588
	// Lets see who we can find that meets the built up conditions
1589
	$members = [];
1590
	$db->fetchQuery('
1591
		SELECT
1592
		 	id_member' . ($details ? ', member_name, real_name, email_address, member_ip, date_registered, last_login,
1593
			posts, is_activated, real_name' : '') . '
1594
		FROM {db_prefix}members
1595
		WHERE ' . $query_where . (!empty($query_params['order']) ? '
1596
		ORDER BY {raw:order}' : '') . (isset($query_params['start']) ? '
1597
		LIMIT {int:start}, {int:limit}' : ''),
1598
		$query_params
1599
	)->fetch_callback(
1600
		function ($row) use (&$members, $details) {
1601
			// Return all the details for each member found
1602
			if ($details)
1603
			{
1604
				$row['id_member'] = (int) $row['id_member'];
1605
				$members[$row['id_member']] = $row;
1606
			}
1607
			// Or just a int[] of found member id's
1608
			else
1609
			{
1610
				$members[] = (int) $row['id_member'];
1611
			}
1612
		}
1613
	);
1614
1615
	return $members;
1616
}
1617
1618
/**
1619
 * Counts the number of members based on conditions
1620
 *
1621
 * @param string[]|string $query see prepareMembersByQuery
1622
 * @param array $query_params see prepareMembersByQuery
1623
 * @param bool $only_active see prepareMembersByQuery
1624
 *
1625
 * @return int
1626
 * @package Members
1627
 *
1628
 */
1629
function countMembersBy($query, $query_params, $only_active = true)
1630
{
1631
	$db = database();
1632
1633
	$query_where = prepareMembersByQuery($query, $query_params, $only_active);
1634
1635
	$request = $db->query('', '
1636
		SELECT 
1637
			COUNT(*)
1638
		FROM {db_prefix}members
1639
		WHERE ' . $query_where,
1640
		$query_params
1641
	);
1642
	list ($num_members) = $request->fetch_row();
1643
	$request->free_result();
1644
1645
	return $num_members;
1646
}
1647
1648
/**
1649
 * Builds the WHERE clause for the functions countMembersBy and membersBy
1650
 *
1651
 * @param array|string $query can be an array of "type" of conditions,
1652
 *             or a string used as raw query
1653
 *             or a string that represents one of the built-in conditions
1654
 *             like member_names, not_in_group, etc
1655
 * @param array $query_params is an array containing the parameters passed to the query
1656
 *             'start' and 'limit' used in LIMIT
1657
 *             'order' used raw in ORDER BY
1658
 *             others passed as query params
1659
 * @param bool $only_active only fetch active members
1660
 *
1661
 * @return bool|mixed|array|string
1662
 * @package Members
1663
 *
1664
 */
1665
function prepareMembersByQuery($query, &$query_params, $only_active = true)
1666
{
1667
	$allowed_conditions = [
1668
		'member_ids' => 'id_member IN ({array_int:member_ids})',
1669
		'member_names' => function (&$members) {
1670
			$mem_query = [];
1671
1672
			foreach ($members['member_names'] as $key => $param)
1673
			{
1674
				$mem_query[] = '{column_case_insensitive:real_name} LIKE {string_case_insensitive:member_names_' . $key . '}';
1675
				$members['member_names_' . $key] = $param;
1676
			}
1677
1678
			return implode("\n\t\t\tOR ", $mem_query);
1679
		},
1680
		'not_in_group' => '(id_group != {int:not_in_group} AND FIND_IN_SET({int:not_in_group}, additional_groups) = 0)',
1681
		'in_group' => '(id_group = {int:in_group} OR FIND_IN_SET({int:in_group}, additional_groups) != 0)',
1682
		'in_group_primary' => 'id_group = {int:in_group_primary}',
1683
		'in_post_group' => 'id_post_group = {int:in_post_group}',
1684
		'in_group_no_add' => '(id_group = {int:in_group_no_add} AND FIND_IN_SET({int:in_group_no_add}, additional_groups) = 0)',
1685
	];
1686
1687
	// Are there multiple parts to this query
1688
	if (is_array($query))
1689
	{
1690
		$query_parts = ['or' => [], 'and' => []];
1691
		foreach ($query as $type => $query_conditions)
1692
		{
1693
			if (is_array($query_conditions))
1694
			{
1695
				foreach ($query_conditions as $condition => $query_condition)
1696
				{
1697
					if ($query_condition == 'member_names')
1698
					{
1699
						$query_parts[$condition === 'or' ? 'or' : 'and'][] = $allowed_conditions[$query_condition]($query_params);
1700
					}
1701
					else
1702
					{
1703
						$query_parts[$condition === 'or' ? 'or' : 'and'][] = $allowed_conditions[$query_condition] ?? $query_condition;
1704
					}
1705
				}
1706
			}
1707
			elseif ($query_conditions == 'member_names')
1708
			{
1709
				$query_parts['and'][] = $allowed_conditions[$query_conditions]($query_params);
1710
			}
1711
			else
1712
			{
1713
				$query_parts['and'][] = $allowed_conditions[$query_conditions] ?? $query_conditions;
1714
			}
1715
		}
1716
1717
		if (!empty($query_parts['or']))
1718
		{
1719
			$query_parts['and'][] = implode("\n\t\t\tOR ", $query_parts['or']);
1720
		}
1721
1722
		$query_where = implode("\n\t\t\tAND ", $query_parts['and']);
1723
	}
1724
	// Is it one of our predefined querys like member_ids, member_names, etc
1725
	elseif (isset($allowed_conditions[$query]))
1726
	{
1727
		$query_where = $query === 'member_names' ? $allowed_conditions[$query]($query_params) : $allowed_conditions[$query];
1728
	}
1729
	// Something else, be careful ;)
1730
	else
1731
	{
1732
		$query_where = $query;
1733
	}
1734
1735
	// Lazy loading, our favorite
1736
	if (empty($query_where))
1737
	{
1738
		return false;
1739
	}
1740
1741
	// Only want active members
1742
	if ($only_active)
1743
	{
1744
		$query_where .= '
1745
			AND is_activated = {int:is_activated}';
1746
		$query_params['is_activated'] = 1;
1747
	}
1748
1749
	return $query_where;
1750
}
1751
1752
/**
1753
 * Retrieve administrators of the site.
1754
 *
1755
 * - The function returns basic information: name, language file.
1756
 * - It is used in personal messages reporting.
1757
 *
1758
 * @param int $id_admin = 0 if requested, only data about a specific admin is retrieved
1759
 *
1760
 * @return array
1761
 * @package Members
1762
 *
1763
 */
1764
function admins($id_admin = 0)
1765
{
1766
	$db = database();
1767
1768
	// Now let's get out and loop through the admins.
1769
	$admins = [];
1770
	$db->fetchQuery('
1771
		SELECT 
1772
			id_member, real_name, lngfile
1773
		FROM {db_prefix}members
1774
		WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1775
			' . (empty($id_admin) ? '' : 'AND id_member = {int:specific_admin}') . '
1776
		ORDER BY real_name, lngfile',
1777
		[
1778
			'admin_group' => 1,
1779
			'specific_admin' => isset($id_admin) ? (int) $id_admin : 0,
1780
		]
1781
	)->fetch_callback(
1782
		function ($row) use (&$admins) {
1783
			$admins[$row['id_member']] = [$row['real_name'], $row['lngfile']];
1784
		}
1785
	);
1786
1787
	return $admins;
1788
}
1789
1790
/**
1791
 * Get the last known id_member
1792
 *
1793
 * @return int
1794
 */
1795
function maxMemberID()
1796
{
1797
	$db = database();
1798
1799
	$request = $db->query('', '
1800
		SELECT 
1801
			MAX(id_member)
1802
		FROM {db_prefix}members',
1803
		[]
1804
	);
1805
	list ($max_id) = $request->fetch_row();
1806
	$request->free_result();
1807
1808
	return $max_id;
1809
}
1810
1811
/**
1812
 * Load some basic member information
1813
 *
1814
 * @param int[]|int $member_ids an array of member IDs or a single ID
1815
 * @param array $options an array of possible little alternatives, can be:
1816
 *  - 'add_guest' (bool) to add a guest user to the returned array
1817
 *  - 'limit' int if set overrides the default query limit
1818
 *  - 'sort' (string) a column to sort the results
1819
 *  - 'moderation' (bool) includes member_ip, id_group, additional_groups, last_login
1820
 *  - 'authentication' (bool) includes secret_answer, secret_question, is_activated, validation_code, passwd_flood, password_salt
1821
 *  - 'preferences' (bool) includes lngfile, mod_prefs, notify_types, signature
1822
 *  - 'lists' (boot) includes buddy_list, pm_ignore_list
1823
 * @return array
1824
 * @package Members
1825
 */
1826
function getBasicMemberData($member_ids, $options = [])
1827
{
1828
	global $txt, $language;
1829
1830
	$db = database();
1831
1832
	$members = [];
1833
	$single = false;
1834
1835
	if (empty($member_ids))
1836
	{
1837
		return [];
1838
	}
1839
1840
	if (!is_array($member_ids))
1841
	{
1842
		$single = true;
1843
		$member_ids = [$member_ids];
1844
	}
1845
1846
	if (!empty($options['add_guest']))
1847
	{
1848
		$single = false;
1849 9
		// This is a guest...
1850
		$members[0] = [
1851 9
			'id_member' => 0,
1852
			'member_name' => '',
1853 9
			'real_name' => $txt['guest_title'],
1854 9
			'email_address' => '',
1855
		];
1856 9
	}
1857
1858
	// Get some additional member info...
1859
	$db->fetchQuery('
1860
		SELECT 
1861 9
			id_member, member_name, real_name, email_address, posts, id_theme' . (!empty($options['moderation']) ? ',
1862
			member_ip, id_group, additional_groups, last_login, id_post_group' : '') . (!empty($options['authentication']) ? ',
1863 3
			secret_answer, secret_question, is_activated, validation_code, passwd_flood, password_salt' : '') . (!empty($options['preferences']) ? ',
1864 3
			lngfile, mod_prefs, notify_types, notify_from, signature' : '') . (!empty($options['lists']) ? ',
1865
			buddy_list, pm_ignore_list' : '') . '
1866
		FROM {db_prefix}members
1867 9
		WHERE id_member IN ({array_int:member_list})
1868
		' . (isset($options['sort']) ? '
1869
		ORDER BY {raw:sort}' : '') . '
1870
		LIMIT {int:limit}',
1871
		[
1872
			'member_list' => $member_ids,
1873
			'limit' => $options['limit'] ?? count($member_ids),
1874
			'sort' => $options['sort'] ?? '',
1875
		]
1876
	)->fetch_callback(
1877
		function ($row) use (&$members, $language, $single, $options) {
1878
			$row['id_member'] = (int) $row['id_member'];
1879
			$row['posts'] = (int) $row['posts'];
1880 9
			$row['id_theme'] = (int) $row['id_theme'];
1881
			if (!empty($options['moderation']))
1882 9
			{
1883 9
				$row['id_group'] = (int) $row['id_group'];
1884 9
				$row['id_post_group'] = (int) $row['id_post_group'];
1885 9
			}
1886
			if (!empty($options['preferences']))
1887
			{
1888 9
				$row['notify_types'] = (int) $row['notify_types'];
1889 9
				$row['notify_from'] = (int) $row['notify_from'];
1890
			}
1891
1892 9
			if (empty($row['lngfile']))
1893 9
			{
1894 9
				$row['lngfile'] = $language;
1895
			}
1896 9
1897
			if (!empty($single))
1898 9
			{
1899
				$members = $row;
1900 9
			}
1901
			else
1902
			{
1903 9
				$members[$row['id_member']] = $row;
1904
			}
1905 3
		}
1906
	);
1907
1908
	return $members;
1909 6
}
1910
1911 9
/**
1912
 * Counts all inactive members
1913
 *
1914 9
 * @return array $inactive_members
1915
 * @package Members
1916
 */
1917
function countInactiveMembers()
1918
{
1919
	$db = database();
1920
1921
	$inactive_members = [];
1922
1923
	$db->fetchQuery('
1924
		SELECT 
1925
			COUNT(*) AS total_members, is_activated
1926
		FROM {db_prefix}members
1927
		WHERE is_activated != {int:is_activated}
1928
		GROUP BY is_activated',
1929
		[
1930
			'is_activated' => 1,
1931
		]
1932
	)->fetch_callback(
1933
		function ($row) use (&$inactive_members) {
1934
			$inactive_members[(int) $row['is_activated']] = (int) $row['total_members'];
1935
		}
1936
	);
1937
1938
	return $inactive_members;
1939
}
1940
1941
/**
1942
 * Get member data by name
1943
 *
1944
 * Retrieves the details of a member by their real name or username. The search is case-insensitive by default,
1945
 * but can be made flexible by setting the $flexible parameter to true.
1946
 *
1947
 * @param string $name The name to search for
1948
 * @param bool $flexible Set to true to enable flexible search
1949
 * @return array|int Returns an array containing the id_member and id_group of the member, or 0 if no member is found
1950
 */
1951
function getMemberByName($name, $flexible = false)
1952
{
1953
	$db = database();
1954
1955
	$request = $db->query('', '
1956
		SELECT 
1957
			id_member, id_group
1958
		FROM {db_prefix}members
1959
		WHERE {column_case_insensitive:real_name} LIKE {string_case_insensitive:name}' . ($flexible ? '
1960
			OR {column_case_insensitive:member_name} LIKE {string_case_insensitive:name}' : '') . '
1961
		LIMIT 1',
1962
		[
1963
			'name' => Util::strtolower($name),
1964
		]
1965
	);
1966
	if ($request->num_rows() === 0)
1967
	{
1968
		return 0;
1969
	}
1970
	$member = $request->fetch_assoc();
1971
	$request->free_result();
1972
1973
	return $member;
1974
}
1975
1976
/**
1977
 * Finds a member from the database using supplied string as real_name
1978
 *
1979
 * - Optionally will only search/find the member in a buddy list
1980
 *
1981
 * @param string $search string to search real_name for like finds
1982
 * @param int[]|null $buddies
1983
 *
1984
 * @return array
1985
 * @package Members
1986
 *
1987
 */
1988
function getMember($search, $buddies = [])
1989
{
1990
	$db = database();
1991
1992
	$xml_data = [
1993
		'items' => [
1994
			'identifier' => 'item',
1995
			'children' => [],
1996
		],
1997
	];
1998
	// Find the member.
1999
	$xml_data['items']['children'] = $db->fetchQuery('
2000
		SELECT 
2001
			id_member, real_name
2002
		FROM {db_prefix}members
2003
		WHERE {column_case_insensitive:real_name} LIKE {string_case_insensitive:search}' . (!empty($buddies) ? '
2004
			AND id_member IN ({array_int:buddy_list})' : '') . '
2005
			AND is_activated IN ({array_int:activation_status})
2006
		ORDER BY LENGTH(real_name), real_name
2007
		LIMIT {int:limit}',
2008
		[
2009
			'buddy_list' => $buddies,
2010
			'search' => $search,
2011
			'activation_status' => [1, 12],
2012
			'limit' => Util::strlen($search) <= 2 ? 100 : 200,
2013
		]
2014
	)->fetch_callback(
2015
		function ($row) {
2016
			$row['real_name'] = strtr($row['real_name'], ['&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;']);
2017
2018
			return [
2019
				'attributes' => [
2020
					'id' => $row['id_member'],
2021
				],
2022
				'value' => $row['real_name'],
2023
			];
2024
		}
2025
	);
2026
2027
	return $xml_data;
2028
}
2029
2030
/**
2031
 * Retrieves MemberData based on conditions
2032
 *
2033
 * @param array $conditions associative array holding the conditions for the WHERE clause of the query.
2034
 * Possible keys:
2035
 * - activated_status (boolean) must be present
2036
 * - time_before (integer)
2037
 * - members (array of integers)
2038
 * - member_greater (integer) a member id, it will be used to filter only members with id_member greater than this
2039
 * - group_list (array) array of group IDs
2040
 * - notify_announcements (integer)
2041
 * - order_by (string)
2042
 * - limit (int)
2043
 * @return array
2044
 * @package Members
2045
 */
2046
function retrieveMemberData($conditions)
2047
{
2048
	// We badly need this
2049
	assert(isset($conditions['activated_status']));
2050
2051
	$db = database();
2052
2053
	$available_conditions = [
2054
		'time_before' => '
2055
				AND date_registered < {int:time_before}',
2056
		'members' => '
2057
				AND id_member IN ({array_int:members})',
2058
		'member_greater' => '
2059
				AND id_member > {int:member_greater}',
2060
		'member_greater_equal' => '
2061
				AND id_member >= {int:member_greater_equal}',
2062
		'member_lesser' => '
2063
				AND id_member < {int:member_lesser}',
2064
		'member_lesser_equal' => '
2065
				AND id_member <= {int:member_lesser_equal}',
2066
		'group_list' => '
2067
				AND (id_group IN ({array_int:group_list}) OR id_post_group IN ({array_int:group_list}) OR FIND_IN_SET({raw:additional_group_list}, additional_groups) != 0)',
2068
		'notify_announcements' => '
2069
				AND notify_announcements = {int:notify_announcements}'
2070
	];
2071
2072
	$query_cond = [];
2073
	foreach ($conditions as $key => $dummy)
2074
	{
2075
		if (isset($available_conditions[$key]))
2076
		{
2077
			$query_cond[] = $available_conditions[$key];
2078
		}
2079
	}
2080
2081
	if (isset($conditions['group_list']))
2082
	{
2083
		$conditions['additional_group_list'] = implode(', additional_groups) != 0 OR FIND_IN_SET(', $conditions['group_list']);
2084
	}
2085
2086
	$data = [];
2087
2088
	if (!isset($conditions['order_by']))
2089
	{
2090
		$conditions['order_by'] = 'lngfile';
2091
	}
2092
2093
	$limit = (isset($conditions['limit'])) ? '
2094
		LIMIT {int:limit}' : '';
2095
2096
	// Get information on each of the members, things that are important to us, like email address...
2097
	$db->fetchQuery('
2098
		SELECT 
2099
			id_member, member_name, real_name, email_address, validation_code, lngfile
2100
		FROM {db_prefix}members
2101
		WHERE is_activated = {int:activated_status}' . implode('', $query_cond) . '
2102
		ORDER BY {raw:order_by}' . $limit,
2103
		$conditions
2104
	)->fetch_callback(
2105
		function ($row) use (&$data) {
2106
			global $modSettings, $language;
2107
2108
			$data['members'][] = (int) $row['id_member'];
2109
			$data['member_info'][] = [
2110
				'id' => (int) $row['id_member'],
2111
				'username' => $row['member_name'],
2112
				'name' => $row['real_name'],
2113
				'email' => $row['email_address'],
2114
				'language' => empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'],
2115
				'code' => $row['validation_code']
2116
			];
2117
		}
2118
	);
2119
2120
	$data['member_count'] = isset($data['members']) ? count($data['members']) : 0;
2121
2122
	return $data;
2123
}
2124
2125
/**
2126
 * Activate members
2127
 *
2128
 * @param array $conditions associative array holding the conditions for the WHERE clause of the query.
2129
 * Possible keys:
2130
 * - activated_status (boolean) must be present
2131
 * - time_before (integer)
2132
 * - members (array of integers)
2133
 *
2134
 * @return int
2135
 * @package Members
2136
 *
2137
 */
2138
function approveMembers($conditions)
2139
{
2140
	$db = database();
2141
2142
	// This shall be present
2143
	assert(isset($conditions['activated_status']));
2144
2145
	$available_conditions = [
2146
		'time_before' => '
2147
				AND date_registered < {int:time_before}',
2148
		'members' => '
2149
				AND id_member IN ({array_int:members})',
2150
	];
2151
2152
	// @todo maybe an hook here?
2153
	$query_cond = [];
2154
	$query = false;
2155
	foreach ($conditions as $key => $dummy)
2156
	{
2157
		if (isset($available_conditions[$key]))
2158
		{
2159
			if ($key === 'time_before')
2160
			{
2161
				$query = true;
2162
			}
2163
			$query_cond[] = $available_conditions[$key];
2164
		}
2165
	}
2166
2167
	if ($query)
2168
	{
2169
		$data = retrieveMemberData($conditions);
2170
		$members_id = [];
2171
		foreach ($data['member_info'] as $member)
2172
		{
2173
			$members_id[] = $member['username'];
2174
		}
2175
	}
2176
	else
2177
	{
2178
		$members_id = $conditions['members'];
2179
	}
2180
2181
	$conditions['is_activated'] = $conditions['activated_status'] >= 10 ? 11 : 1;
2182
	$conditions['blank_string'] = '';
2183
2184
	// Approve/activate this member.
2185
	$db->query('', '
2186
		UPDATE {db_prefix}members
2187
		SET validation_code = {string:blank_string}, is_activated = {int:is_activated}
2188
		WHERE is_activated = {int:activated_status}' . implode('', $query_cond),
2189
		$conditions
2190
	);
2191
2192
	// Let the integration know that they've been activated!
2193
	foreach ($members_id as $member_id)
2194
	{
2195
		call_integration_hook('integrate_activate', [$member_id, $conditions['activated_status'], $conditions['is_activated']]);
2196
	}
2197
2198
	return $conditions['is_activated'];
2199
}
2200
2201
/**
2202
 * Set these members for activation
2203
 *
2204
 * @param array $conditions associative array holding the conditions for the  WHERE clause of the query.
2205
 * Possible keys:
2206
 * - selected_member (integer) must be present
2207
 * - activated_status (boolean) must be present
2208
 * - validation_code (string) must be present
2209
 * - members (array of integers)
2210
 * - time_before (integer)
2211
 * @package Members
2212
 */
2213
function enforceReactivation($conditions)
2214
{
2215
	$db = database();
2216
2217
	// We need all of these
2218
	assert(isset($conditions['activated_status']));
2219
	assert(isset($conditions['selected_member']));
2220
	assert(isset($conditions['validation_code']));
2221
2222
	$conditions['validation_code'] = substr(hash('sha256', $conditions['validation_code']), 0, 10);
2223
2224
	$available_conditions = [
2225
		'time_before' => '
2226
				AND date_registered < {int:time_before}',
2227
		'members' => '
2228
				AND id_member IN ({array_int:members})',
2229
	];
2230
2231
	$query_cond = [];
2232
	foreach ($conditions as $key => $dummy)
2233
	{
2234
		if (isset($available_conditions[$key]))
2235
		{
2236
			$query_cond[] = $available_conditions[$key];
2237
		}
2238
	}
2239
2240
	$conditions['not_activated'] = 0;
2241
2242
	$db->query('', '
2243
		UPDATE {db_prefix}members
2244
		SET validation_code = {string:validation_code}, is_activated = {int:not_activated}
2245
		WHERE is_activated = {int:activated_status}
2246
			' . implode('', $query_cond) . '
2247
			AND id_member = {int:selected_member}',
2248
		$conditions
2249
	);
2250
}
2251
2252
/**
2253
 * Count members of a given group
2254
 *
2255
 * @param int $id_group
2256
 * @return int
2257
 * @package Members
2258
 */
2259
function countMembersInGroup($id_group = 0)
2260
{
2261
	$db = database();
2262
2263
	// Determine the number of ungrouped members.
2264
	$request = $db->query('', '
2265
		SELECT
2266
		 	COUNT(*)
2267
		FROM {db_prefix}members
2268
		WHERE id_group = {int:group}',
2269
		[
2270
			'group' => $id_group,
2271
		]
2272
	);
2273
	list ($num_members) = $request->fetch_row();
2274
	$request->free_result();
2275
2276
	return $num_members;
2277
}
2278
2279
/**
2280
 * Get the total amount of members online.
2281
 *
2282
 * @param string[] $conditions
2283
 * @return int
2284
 * @package Members
2285
 */
2286
function countMembersOnline($conditions)
2287
{
2288
	$db = database();
2289
2290
	$request = $db->query('', '
2291
		SELECT 
2292
			COUNT(*)
2293
		FROM {db_prefix}log_online AS lo
2294
			LEFT JOIN {db_prefix}members AS mem ON (lo.id_member = mem.id_member)' . (!empty($conditions) ? '
2295
		WHERE ' . implode(' AND ', $conditions) : ''),
2296
		[]
2297
	);
2298
	list ($totalMembers) = $request->fetch_row();
2299
	$request->free_result();
2300
2301
	return $totalMembers;
2302
}
2303
2304
/**
2305
 * Look for people online, provided they don't mind if you see they are.
2306
 *
2307
 * @param string[] $conditions
2308
 * @param string $sort_method
2309
 * @param string $sort_direction
2310
 * @param int $start
2311
 * @return array
2312
 * @package Members
2313
 */
2314
function onlineMembers($conditions, $sort_method, $sort_direction, $start)
2315
{
2316
	global $modSettings;
2317
2318
	$db = database();
2319
2320
	return $db->fetchQuery('
2321
		SELECT
2322
			lo.log_time, lo.id_member, lo.url, lo.ip, mem.real_name,
2323
			lo.session, mg.online_color, COALESCE(mem.show_online, 1) AS show_online,
2324
			lo.id_spider
2325
		FROM {db_prefix}log_online AS lo
2326
			LEFT JOIN {db_prefix}members AS mem ON (lo.id_member = mem.id_member)
2327
			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:regular_member} THEN mem.id_post_group ELSE mem.id_group END)' . (!empty($conditions) ? '
2328
		WHERE ' . implode(' AND ', $conditions) : '') . '
2329
		ORDER BY {raw:sort_method} {raw:sort_direction}
2330
		LIMIT {int:limit} OFFSET {int:offset}',
2331
		[
2332
			'regular_member' => 0,
2333
			'sort_method' => $sort_method,
2334
			'sort_direction' => $sort_direction === 'up' ? 'ASC' : 'DESC',
2335
			'offset' => $start,
2336
			'limit' => $modSettings['defaultMaxMembers'],
2337
		]
2338
	)->fetch_all();
2339
}
2340
2341
/**
2342
 * Find the most recent members
2343
 *
2344
 * @param int $limit
2345
 *
2346
 * @return array
2347
 * @package Members
2348
 *
2349
 */
2350
function recentMembers($limit)
2351
{
2352
	$db = database();
2353
2354
	// Find the most recent members.
2355
	return $db->fetchQuery('
2356
		SELECT 
2357
			id_member, member_name, real_name, date_registered, last_login
2358
		FROM {db_prefix}members
2359
		ORDER BY id_member DESC
2360
		LIMIT {int:limit}',
2361
		[
2362
			'limit' => $limit,
2363
		]
2364
	)->fetch_all();
2365
}
2366
2367
/**
2368
 * Assign membergroups to members.
2369
 *
2370
 * @param int $member
2371
 * @param int $primary_group
2372
 * @param int[] $additional_groups
2373
 * @package Members
2374
 */
2375
function assignGroupsToMember($member, $primary_group, $additional_groups)
2376
{
2377
	updateMemberData($member, ['id_group' => $primary_group, 'additional_groups' => implode(',', $additional_groups)]);
2378
}
2379
2380
/**
2381
 * Get a list of members from a membergroups request.
2382
 *
2383
 * @param int[] $groups
2384
 * @param string $where
2385
 * @param bool $change_groups = false
2386
 * @return mixed
2387
 * @package Members
2388
 */
2389
function getConcernedMembers($groups, $where, $change_groups = false)
2390
{
2391
	$db = database();
2392
2393
	// Get the details of all the members concerned...
2394
	$email_details = [];
2395
	$group_changes = [];
2396
	$db->fetchQuery('
2397
		SELECT 
2398
			lgr.id_request, lgr.id_member, lgr.id_group, mem.email_address, mem.id_group AS primary_group,
2399
			mem.additional_groups AS additional_groups, mem.lngfile, mem.member_name, mem.notify_types,
2400
			mg.hidden, mg.group_name
2401
		FROM {db_prefix}log_group_requests AS lgr
2402
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = lgr.id_member)
2403
			INNER JOIN {db_prefix}membergroups AS mg ON (mg.id_group = lgr.id_group)
2404
		WHERE ' . $where . '
2405
			AND lgr.id_request IN ({array_int:request_list})
2406
		ORDER BY mem.lngfile',
2407
		[
2408
			'request_list' => $groups,
2409
		]
2410
	)->fetch_callback(
2411
		function ($row) use (&$email_details, &$group_changes, $change_groups) {
2412
			global $modSettings, $language;
2413
2414
			$row['lngfile'] = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
2415
2416
			// If we are approving work out what their new group is.
2417
			if ($change_groups)
2418
			{
2419
				// For people with more than one request at once.
2420
				if (isset($group_changes[$row['id_member']]))
2421
				{
2422
					$row['additional_groups'] = $group_changes[$row['id_member']]['add'];
2423
					$row['primary_group'] = $group_changes[$row['id_member']]['primary'];
2424
				}
2425
				else
2426
				{
2427
					$row['additional_groups'] = explode(',', $row['additional_groups']);
2428
				}
2429
2430
				// Don't have it already?
2431
				if ($row['primary_group'] == $row['id_group'] || in_array($row['id_group'], $row['additional_groups']))
2432
				{
2433
					return;
2434
				}
2435
2436
				// Should it become their primary?
2437
				if ($row['primary_group'] == 0 && $row['hidden'] == 0)
2438
				{
2439
					$row['primary_group'] = $row['id_group'];
2440
				}
2441
				else
2442
				{
2443
					$row['additional_groups'][] = $row['id_group'];
2444
				}
2445
2446
				// Add them to the group master list.
2447
				$group_changes[$row['id_member']] = [
2448
					'primary' => $row['primary_group'],
2449
					'add' => $row['additional_groups'],
2450
				];
2451
			}
2452
2453
			// Add required information to email them.
2454
			if ($row['notify_types'] != 4)
2455
			{
2456
				$email_details[] = [
2457
					'rid' => $row['id_request'],
2458
					'member_id' => $row['id_member'],
2459
					'member_name' => $row['member_name'],
2460
					'group_id' => $row['id_group'],
2461
					'group_name' => $row['group_name'],
2462
					'email' => $row['email_address'],
2463
					'language' => $row['lngfile'],
2464
				];
2465
			}
2466
		}
2467
	);
2468
2469
	return [
2470
		'email_details' => $email_details,
2471
		'group_changes' => $group_changes
2472
	];
2473
}
2474
2475
/**
2476
 * Determine if the current user (User::$info) can contact another user ($who)
2477
 *
2478
 * @param int $who The id of the user to contact
2479
 *
2480
 * @return bool
2481
 * @package Members
2482
 *
2483
 */
2484
function canContact($who)
2485
{
2486
	$db = database();
2487
2488
	$request = $db->query('', '
2489
		SELECT 
2490
			receive_from, buddy_list, pm_ignore_list
2491
		FROM {db_prefix}members
2492
		WHERE id_member = {int:member}',
2493
		[
2494
			'member' => $who,
2495
		]
2496
	);
2497
	list ($receive_from, $buddies, $ignore) = $request->fetch_row();
2498
	$request->free_result();
2499
2500
	$buddy_list = array_map('intval', explode(',', $buddies));
2501
	$ignore_list = array_map('intval', explode(',', $ignore));
2502
2503
	// 0 = all members
2504
	if ($receive_from == 0)
2505
	{
2506
		return true;
2507
	}
2508
2509
	// 1 = all except ignore
2510
	if ($receive_from == 1)
2511
	{
2512
		return !(!empty($ignore_list) && in_array(User::$info->id, $ignore_list));
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...
2513
	}
2514
2515
	// 2 = buddies and admin
2516
	if ($receive_from == 2)
2517
	{
2518
		return (User::$info->is_admin || (!empty($buddy_list) && in_array(User::$info->id, $buddy_list)));
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...
2519
	}
2520
2521
	// 3 = admin only
2522
	return (bool) User::$info->is_admin;
2523
}
2524
2525
/**
2526
 * This function updates the latest member, the total membercount, and the
2527
 * number of unapproved members.
2528
 *
2529
 * - It also only counts approved members when approval is on,
2530
 * but is much more efficient with it off.
2531
 *
2532
 * @param int|null $id_member = null If not an integer reload from the database
2533
 * @param string|null $real_name = null
2534
 * @package Members
2535
 */
2536
function updateMemberStats($id_member = null, $real_name = null)
2537
{
2538
	global $modSettings;
2539
2540
	$db = database();
2541
2542
	$changes = [
2543
		'memberlist_updated' => time(),
2544
	];
2545
2546
	// #1 latest member ID, #2 the real name for a new registration.
2547
	if (is_int($id_member))
2548
	{
2549
		$changes['latestMember'] = $id_member;
2550
		$changes['latestRealName'] = $real_name;
2551
2552
		updateSettings(['totalMembers' => true], true);
2553
	}
2554
	// We need to calculate the totals.
2555
	else
2556
	{
2557
		// Update the latest activated member (highest id_member) and count.
2558
		$request = $db->query('', '
2559
			SELECT
2560
			 	COUNT(*), MAX(id_member)
2561
			FROM {db_prefix}members
2562
			WHERE is_activated = {int:is_activated}',
2563
			[
2564
				'is_activated' => 1,
2565
			]
2566
		);
2567
		list ($changes['totalMembers'], $changes['latestMember']) = $request->fetch_row();
2568
		$request->free_result();
2569
2570
		// Get the latest activated member's display name.
2571
		$request = getBasicMemberData((int) $changes['latestMember']);
2572
		$changes['latestRealName'] = $request['real_name'];
2573
2574
		// Are we using registration approval?
2575
		if ((!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 2) || !empty($modSettings['approveAccountDeletion']))
2576
		{
2577
			// Update the amount of members awaiting approval - ignoring COPPA accounts, as you can't approve them until you get permission.
2578
			$request = $db->query('', '
2579
				SELECT 
2580
					COUNT(*)
2581
				FROM {db_prefix}members
2582
				WHERE is_activated IN ({array_int:activation_status})',
2583 6
				[
2584
					'activation_status' => [3, 4],
2585 6
				]
2586
			);
2587
			list ($changes['unapprovedMembers']) = $request->fetch_row();
2588 6
			$request->free_result();
2589
		}
2590
2591
		// What about unapproved COPPA registrations?
2592 6
		if (!empty($modSettings['coppaType']) && $modSettings['coppaType'] != 1)
2593
		{
2594 3
			$request = $db->query('', '
2595 3
				SELECT 
2596
					COUNT(*)
2597 3
				FROM {db_prefix}members
2598
				WHERE is_activated = {int:coppa_approval}',
2599
				[
2600
					'coppa_approval' => 5,
2601
				]
2602
			);
2603 3
			list ($coppa_approvals) = $request->fetch_row();
2604
			$request->free_result();
2605
2606
			// Add this to the number of unapproved members
2607
			if (!empty($changes['unapprovedMembers']))
2608
			{
2609 3
				$changes['unapprovedMembers'] += $coppa_approvals;
2610
			}
2611
			else
2612 3
			{
2613 3
				$changes['unapprovedMembers'] = $coppa_approvals;
2614
			}
2615
		}
2616 3
	}
2617 3
2618
	updateSettings($changes);
2619
}
2620 3
2621
/**
2622
 * Builds the 'query_see_board' element for a certain member
2623
 *
2624
 * @param int $id_member a valid member id
2625
 * @return string Query string
2626
 * @package Members
2627
 */
2628
function memberQuerySeeBoard($id_member)
2629
{
2630
	global $modSettings;
2631
2632
	$member = getBasicMemberData($id_member, ['moderation' => true]);
2633
	if (empty($member))
2634
	{
2635
		return '0=1';
2636
	}
2637 6
2638 6
	if (empty($member['additional_groups']))
2639
	{
2640
		$groups = [$member['id_group'], $member['id_post_group']];
2641
	}
2642
	else
2643
	{
2644
		$groups = array_merge(
2645
			[$member['id_group'], $member['id_post_group']],
2646
			explode(',', $member['additional_groups'])
2647
		);
2648
	}
2649
2650
	foreach ($groups as $k => $v)
2651
	{
2652
		$groups[$k] = (int) $v;
2653
	}
2654
	$groups = array_unique($groups);
2655
2656
	if (in_array(1, $groups))
2657
	{
2658
		return '1=1';
2659
	}
2660
2661
	require_once(SUBSDIR . '/Boards.subs.php');
2662
2663
	$boards_mod = boardsModerated($id_member);
2664
	$mod_query = empty($boards_mod) ? '' : ' OR b.id_board IN (' . implode(',', $boards_mod) . ')';
2665
2666
	return '((FIND_IN_SET(' . implode(', b.member_groups) != 0 OR FIND_IN_SET(', $groups) . ', b.member_groups) != 0)' . (!empty($modSettings['deny_boards_access']) ? ' AND (FIND_IN_SET(' . implode(', b.deny_member_groups) = 0 AND FIND_IN_SET(', $groups) . ', b.deny_member_groups) = 0)' : '') . $mod_query . ')';
2667
}
2668
2669
/**
2670
 * Updates the columns in the members table.
2671
 *
2672
 * What it does:
2673
 *
2674
 * - Assumes the data has been htmlspecialchar'd, no sanitation is performed on the data.
2675
 * - This function should be used whenever member data needs to be updated in place of an UPDATE query.
2676
 * - $data is an associative array of the columns to be updated and their respective values.
2677
 * any string values updated should be quoted and slashed.
2678
 * - The value of any column can be '+' or '-', which mean 'increment' and decrement, respectively.
2679
 * - If the member's post number is updated, updates their post groups.
2680
 *
2681
 * @param int[]|int $members An array of member ids
2682
 * @param array $data An associative array of the columns to be updated and their respective values.
2683
 */
2684
function updateMemberData($members, $data)
2685
{
2686
	global $modSettings;
2687
2688
	$db = database();
2689
2690
	$parameters = [];
2691
	if (is_array($members))
2692
	{
2693
		$condition = 'id_member IN ({array_int:members})';
2694
		$parameters['members'] = $members;
2695
	}
2696
	elseif ($members === null)
0 ignored issues
show
introduced by
The condition $members === null is always false.
Loading history...
2697
	{
2698
		$condition = '1=1';
2699
	}
2700
	else
2701
	{
2702
		$condition = 'id_member = {int:member}';
2703
		$parameters['member'] = $members;
2704
	}
2705
2706
	// Everything is assumed to be a string unless it's in the below.
2707
	$knownInts = [
2708
		'date_registered', 'posts', 'id_group', 'last_login', 'personal_messages', 'unread_messages', 'mentions',
2709 28
		'new_pm', 'pm_prefs', 'show_online', 'pm_email_notify', 'receive_from', 'karma_good', 'karma_bad',
2710
		'notify_announcements', 'notify_send_body', 'notify_regularity', 'notify_types', 'notify_from',
2711 28
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning', 'likes_given',
2712
		'likes_received', 'enable_otp', 'otp_used'
2713 28
	];
2714 28
	$knownFloats = [
2715
		'time_offset',
2716 2
	];
2717 2
2718
	if (!empty($modSettings['integrate_change_member_data']))
2719 26
	{
2720
		// Only a few member variables are really interesting for integration.
2721
		$integration_vars = [
2722
			'member_name',
2723
			'real_name',
2724
			'email_address',
2725 26
			'id_group',
2726 26
			'birthdate',
2727
			'website_title',
2728
			'website_url',
2729
			'time_format',
2730
			'time_offset',
2731 28
			'avatar',
2732
			'lngfile',
2733
		];
2734
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
2735
2736
		// Only proceed if there are any variables left to call the integration function.
2737
		if (count($vars_to_integrate) !== 0)
2738 28
		{
2739
			// Fetch a list of member_names if necessary
2740
			if ((!is_array($members) && $members === User::$info->id) || (is_array($members) && count($members) == 1 && in_array(User::$info->id, $members)))
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...
2741 28
			{
2742
				$member_names = [User::$info->username];
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2743
			}
2744
			else
2745
			{
2746
				$member_names = $db->fetchQuery('
2747
					SELECT 
2748
						member_name
2749
					FROM {db_prefix}members
2750
					WHERE ' . $condition,
2751
					$parameters
2752
				)->fetch_callback(
2753
					function ($row) {
2754
						return $row['member_name'];
2755
					}
2756
				);
2757
			}
2758
2759
			if (!empty($member_names))
2760
			{
2761
				foreach ($vars_to_integrate as $var)
2762
				{
2763
					call_integration_hook('integrate_change_member_data', [$member_names, &$var, &$data[$var], &$knownInts, &$knownFloats]);
2764
				}
2765
			}
2766
		}
2767
	}
2768
2769
	$setString = '';
2770
	foreach ($data as $var => $val)
2771
	{
2772
		$type = 'string';
2773
2774
		if (in_array($var, $knownInts))
2775
		{
2776
			$type = 'int';
2777
		}
2778
		elseif (in_array($var, $knownFloats))
2779
		{
2780
			$type = 'float';
2781
		}
2782
		elseif ($var === 'birthdate')
2783
		{
2784
			$type = 'date';
2785
		}
2786
2787
		// Doing an increment?
2788
		if ($type === 'int' && ($val === '+' || $val === '-'))
2789
		{
2790
			$val = $var . ' ' . $val . ' 1';
2791
			$type = 'raw';
2792
		}
2793 28
2794 28
		// Ensure posts, personal_messages, and unread_messages don't overflow or underflow.
2795
		if (in_array($var, ['posts', 'personal_messages', 'unread_messages']))
2796 28
		{
2797
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
2798 28
			{
2799
				if ($match[1] !== '+ ')
2800 28
				{
2801
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
2802 2
				}
2803
				$type = 'raw';
2804
			}
2805
		}
2806 2
2807
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
2808
		$parameters['p_' . $var] = $val;
2809
	}
2810
2811
	$db->query('', '
2812 28
		UPDATE {db_prefix}members
2813
		SET' . substr($setString, 0, -1) . '
2814 16
		WHERE ' . $condition,
2815 16
		$parameters
2816
	);
2817
2818
	require_once(SUBSDIR . '/Membergroups.subs.php');
2819 28
	updatePostGroupStats($members, array_keys($data));
0 ignored issues
show
Bug introduced by
It seems like $members can also be of type integer; however, parameter $members of updatePostGroupStats() does only seem to accept integer[]|null, maybe add an additional type check? ( Ignorable by Annotation )

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

2819
	updatePostGroupStats(/** @scrutinizer ignore-type */ $members, array_keys($data));
Loading history...
2820
2821 18
	$cache = Cache::instance();
2822
2823 18
	// Clear any caching?
2824
	if ($cache->levelHigherThan(1) && !empty($members))
2825 12
	{
2826
		if (!is_array($members))
2827 18
		{
2828
			$members = [$members];
2829
		}
2830
2831 28
		foreach ($members as $member)
2832 28
		{
2833
			if ($cache->levelHigherThan(2))
2834
			{
2835 28
				$cache->remove('member_data-profile-' . $member);
2836
				$cache->remove('member_data-normal-' . $member);
2837 28
				$cache->remove('member_data-minimal-' . $member);
2838 28
			}
2839 14
2840
			$cache->remove('user_settings-' . $member);
2841
		}
2842 28
	}
2843 28
}
2844
2845 28
/**
2846
 * Loads members who are associated with an ip address
2847
 *
2848 28
 * @param string $ip_string raw value to use in where clause
2849
 * @param string $ip_var
2850
 *
2851
 * @return array
2852
 */
2853
function loadMembersIPs($ip_string, $ip_var)
2854
{
2855
	$db = database();
2856
2857
	$ips = [];
2858
	$db->fetchQuery('
2859
		SELECT
2860
			id_member, real_name AS display_name, member_ip
2861
		FROM {db_prefix}members
2862
		WHERE member_ip ' . $ip_string,
2863
		[
2864
			'ip_address' => $ip_var,
2865
		]
2866
	)->fetch_callback(
2867 28
		function ($row) use (&$ips) {
2868
			$ips[$row['member_ip']][] = '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['display_name']]) . '">' . $row['display_name'] . '</a>';
2869
		}
2870
	);
2871
2872
	ksort($ips);
2873
2874
	return $ips;
2875
}
2876
2877
/**
2878
 * Logs when teh user accepted the site agreement
2879
 *
2880
 * @param int $id_member
2881
 * @param string $ip
2882
 * @param string $agreement_version
2883
 */
2884
function registerAgreementAccepted($id_member, $ip, $agreement_version)
2885
{
2886
	$db = database();
2887
2888
	$db->insert('',
2889
		'{db_prefix}log_agreement_accept',
2890
		[
2891
			'version' => 'date',
2892
			'id_member' => 'int',
2893
			'accepted_date' => 'date',
2894
			'accepted_ip' => 'string-255',
2895
		],
2896
		[
2897
			[
2898
				'version' => $agreement_version,
2899
				'id_member' => $id_member,
2900
				'accepted_date' => Util::strftime('%Y-%m-%d', forum_time(false)),
2901
				'accepted_ip' => $ip,
2902
			]
2903
		],
2904
		['version', 'id_member']
2905
	);
2906
}
2907