groupsAllowedTo()   B
last analyzed

Complexity

Conditions 8
Paths 5

Size

Total Lines 74
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 17.1976

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 31
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 74
ccs 10
cts 21
cp 0.4762
crap 17.1976
rs 8.1795

How to fix   Long Method   

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

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

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

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