membersByIP()   B
last analyzed

Complexity

Conditions 9
Paths 24

Size

Total Lines 55
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9.0139

Importance

Changes 0
Metric Value
cc 9
eloc 27
nc 24
nop 3
dl 0
loc 55
ccs 17
cts 18
cp 0.9444
crap 9.0139
rs 8.0555
c 0
b 0
f 0

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