Completed
Push — patch_1-1-7 ( 68a437...e35bbb )
by Emanuele
39:30 queued 24:32
created

updateMemberSalt()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 9
c 1
b 0
f 1
nc 3
nop 3
dl 0
loc 19
rs 9.9666
1
<?php
2
3
/**
4
 * This file contains some useful functions for members and membergroups.
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * This file contains code covered by:
11
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
12
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
13
 *
14
 * @version 1.1.7
15
 *
16
 */
17
18
/**
19
 * Delete one or more members.
20
 *
21
 * What it does:
22
 *
23
 * - Requires profile_remove_own or profile_remove_any permission for
24
 * respectively removing your own account or any account.
25
 * - Non-admins cannot delete admins.
26
 *
27
 * What id does
28
 * - Changes author of messages, topics and polls to guest authors.
29
 * - Removes all log entries concerning the deleted members, except the
30
 * error logs, ban logs and moderation logs.
31
 * - Removes these members' personal messages (only the inbox)
32
 * - Removes avatars, ban entries, theme settings, moderator positions, poll votes,
33
 * likes, mentions, notifications
34
 * - Removes custom field data associated with them
35
 * - Updates member statistics afterwards.
36
 *
37
 * @package Members
38
 * @param int[]|int $users
39
 * @param bool $check_not_admin = false
40
 * @throws Elk_Exception
41
 */
42
function deleteMembers($users, $check_not_admin = false)
43
{
44
	global $modSettings, $user_info;
45
46
	$db = database();
47
48
	// Try give us a while to sort this out...
49
	detectServer()->setTimeLimit(600);
50
51
	// Try to get some more memory.
52
	detectServer()->setMemoryLimit('128M');
53
54
	// If it's not an array, make it so!
55
	if (!is_array($users))
56
		$users = array($users);
57
	else
58
		$users = array_unique($users);
59
60
	// Make sure there's no void user in here.
61
	$users = array_diff($users, array(0));
62
63
	// How many are they deleting?
64
	if (empty($users))
65
		return;
66
	elseif (count($users) == 1)
67
	{
68
		list ($user) = $users;
69
70
		if ($user == $user_info['id'])
71
			isAllowedTo('profile_remove_own');
72
		else
73
			isAllowedTo('profile_remove_any');
74
	}
75
	else
76
	{
77
		foreach ($users as $k => $v)
78
			$users[$k] = (int) $v;
79
80
		// Deleting more than one?  You can't have more than one account...
81
		isAllowedTo('profile_remove_any');
82
	}
83
84
	// Get their names for logging purposes.
85
	$request = $db->query('', '
86
		SELECT 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
87
		FROM {db_prefix}members
88
		WHERE id_member IN ({array_int:user_list})
89
		LIMIT ' . count($users),
90
		array(
91
			'user_list' => $users,
92
			'admin_group' => 1,
93
		)
94
	);
95
	$admins = array();
96
	$emails = array();
97
	$user_log_details = array();
98
	while ($row = $db->fetch_assoc($request))
99
	{
100
		if ($row['is_admin'])
101
			$admins[] = $row['id_member'];
102
		$user_log_details[$row['id_member']] = array($row['id_member'], $row['member_name']);
103
		$emails[] = $row['email_address'];
104
	}
105
	$db->free_result($request);
106
107
	if (empty($user_log_details))
108
		return;
109
110
	// Make sure they aren't trying to delete administrators if they aren't one.  But don't bother checking if it's just themselves.
111
	if (!empty($admins) && ($check_not_admin || (!allowedTo('admin_forum') && (count($users) != 1 || $users[0] != $user_info['id']))))
112
	{
113
		$users = array_diff($users, $admins);
114
		foreach ($admins as $id)
115
			unset($user_log_details[$id]);
116
	}
117
118
	// No one left?
119
	if (empty($users))
120
		return;
121
122
	// Log the action - regardless of who is deleting it.
123
	$log_changes = array();
124
	foreach ($user_log_details as $user)
125
	{
126
		$log_changes[] = array(
127
			'action' => 'delete_member',
128
			'log_type' => 'admin',
129
			'extra' => array(
130
				'member' => $user[0],
131
				'name' => $user[1],
132
				'member_acted' => $user_info['name'],
133
			),
134
		);
135
136
		// Remove any cached data if enabled.
137
		Cache::instance()->remove('user_settings-' . $user[0]);
138
	}
139
140
	// Make these peoples' posts guest posts.
141
	$db->query('', '
142
		UPDATE {db_prefix}messages
143
		SET id_member = {int:guest_id}' . (!empty($modSettings['deleteMembersRemovesEmail']) ? ',
144
		poster_email = {string:blank_email}' : '') . '
145
		WHERE id_member IN ({array_int:users})',
146
		array(
147
			'guest_id' => 0,
148
			'blank_email' => '',
149
			'users' => $users,
150
		)
151
	);
152
	$db->query('', '
153
		UPDATE {db_prefix}polls
154
		SET id_member = {int:guest_id}
155
		WHERE id_member IN ({array_int:users})',
156
		array(
157
			'guest_id' => 0,
158
			'users' => $users,
159
		)
160
	);
161
162
	// Make these peoples' posts guest first posts and last posts.
163
	$db->query('', '
164
		UPDATE {db_prefix}topics
165
		SET id_member_started = {int:guest_id}
166
		WHERE id_member_started IN ({array_int:users})',
167
		array(
168
			'guest_id' => 0,
169
			'users' => $users,
170
		)
171
	);
172
	$db->query('', '
173
		UPDATE {db_prefix}topics
174
		SET id_member_updated = {int:guest_id}
175
		WHERE id_member_updated IN ({array_int:users})',
176
		array(
177
			'guest_id' => 0,
178
			'users' => $users,
179
		)
180
	);
181
182
	$db->query('', '
183
		UPDATE {db_prefix}log_actions
184
		SET id_member = {int:guest_id}
185
		WHERE id_member IN ({array_int:users})',
186
		array(
187
			'guest_id' => 0,
188
			'users' => $users,
189
		)
190
	);
191
192
	$db->query('', '
193
		UPDATE {db_prefix}log_banned
194
		SET id_member = {int:guest_id}
195
		WHERE id_member IN ({array_int:users})',
196
		array(
197
			'guest_id' => 0,
198
			'users' => $users,
199
		)
200
	);
201
202
	$db->query('', '
203
		UPDATE {db_prefix}log_errors
204
		SET id_member = {int:guest_id}
205
		WHERE id_member IN ({array_int:users})',
206
		array(
207
			'guest_id' => 0,
208
			'users' => $users,
209
		)
210
	);
211
212
	// Delete the member.
213
	$db->query('', '
214
		DELETE FROM {db_prefix}members
215
		WHERE id_member IN ({array_int:users})',
216
		array(
217
			'users' => $users,
218
		)
219
	);
220
221
	// Delete any likes...
222
	$db->query('', '
223
		DELETE FROM {db_prefix}message_likes
224
		WHERE id_member IN ({array_int:users})',
225
		array(
226
			'users' => $users,
227
		)
228
	);
229
230
	// Delete any custom field data...
231
	$db->query('', '
232
		DELETE FROM {db_prefix}custom_fields_data
233
		WHERE id_member IN ({array_int:users})',
234
		array(
235
			'users' => $users,
236
		)
237
	);
238
239
	// Delete any post by email keys...
240
	$db->query('', '
241
		DELETE FROM {db_prefix}postby_emails
242
		WHERE email_to IN ({array_string:emails})',
243
		array(
244
			'emails' => $emails,
245
		)
246
	);
247
248
	// Delete the logs...
249
	$db->query('', '
250
		DELETE FROM {db_prefix}log_actions
251
		WHERE id_log = {int:log_type}
252
			AND id_member IN ({array_int:users})',
253
		array(
254
			'log_type' => 2,
255
			'users' => $users,
256
		)
257
	);
258
	$db->query('', '
259
		DELETE FROM {db_prefix}log_boards
260
		WHERE id_member IN ({array_int:users})',
261
		array(
262
			'users' => $users,
263
		)
264
	);
265
	$db->query('', '
266
		DELETE FROM {db_prefix}log_comments
267
		WHERE id_recipient IN ({array_int:users})
268
			AND comment_type = {string:warntpl}',
269
		array(
270
			'users' => $users,
271
			'warntpl' => 'warntpl',
272
		)
273
	);
274
	$db->query('', '
275
		DELETE FROM {db_prefix}log_group_requests
276
		WHERE id_member IN ({array_int:users})',
277
		array(
278
			'users' => $users,
279
		)
280
	);
281
	$db->query('', '
282
		DELETE FROM {db_prefix}log_karma
283
		WHERE id_target IN ({array_int:users})
284
			OR id_executor IN ({array_int:users})',
285
		array(
286
			'users' => $users,
287
		)
288
	);
289
	$db->query('', '
290
		DELETE FROM {db_prefix}log_mark_read
291
		WHERE id_member IN ({array_int:users})',
292
		array(
293
			'users' => $users,
294
		)
295
	);
296
	$db->query('', '
297
		DELETE FROM {db_prefix}log_notify
298
		WHERE id_member IN ({array_int:users})',
299
		array(
300
			'users' => $users,
301
		)
302
	);
303
	$db->query('', '
304
		DELETE FROM {db_prefix}log_online
305
		WHERE id_member IN ({array_int:users})',
306
		array(
307
			'users' => $users,
308
		)
309
	);
310
	$db->query('', '
311
		DELETE FROM {db_prefix}log_subscribed
312
		WHERE id_member IN ({array_int:users})',
313
		array(
314
			'users' => $users,
315
		)
316
	);
317
	$db->query('', '
318
		DELETE FROM {db_prefix}log_topics
319
		WHERE id_member IN ({array_int:users})',
320
		array(
321
			'users' => $users,
322
		)
323
	);
324
	$db->query('', '
325
		DELETE FROM {db_prefix}collapsed_categories
326
		WHERE id_member IN ({array_int:users})',
327
		array(
328
			'users' => $users,
329
		)
330
	);
331
332
	// Make their votes appear as guest votes - at least it keeps the totals right.
333
	// @todo Consider adding back in cookie protection.
334
	$db->query('', '
335
		UPDATE {db_prefix}log_polls
336
		SET id_member = {int:guest_id}
337
		WHERE id_member IN ({array_int:users})',
338
		array(
339
			'guest_id' => 0,
340
			'users' => $users,
341
		)
342
	);
343
344
	// Remove the mentions
345
	$db->query('', '
346
		DELETE FROM {db_prefix}log_mentions
347
		WHERE id_member IN ({array_int:users})',
348
		array(
349
			'users' => $users,
350
		)
351
	);
352
	// And null all those that were added by him
353
	$db->query('', '
354
		UPDATE {db_prefix}log_mentions
355
		SET id_member_from = {int:zero}
356
		WHERE id_member_from IN ({array_int:users})',
357
		array(
358
			'zero' => 0,
359
			'users' => $users,
360
		)
361
	);
362
363
	// Delete personal messages.
364
	require_once(SUBSDIR . '/PersonalMessage.subs.php');
365
	deleteMessages(null, null, $users);
366
367
	$db->query('', '
368
		UPDATE {db_prefix}personal_messages
369
		SET id_member_from = {int:guest_id}
370
		WHERE id_member_from IN ({array_int:users})',
371
		array(
372
			'guest_id' => 0,
373
			'users' => $users,
374
		)
375
	);
376
377
	// They no longer exist, so we don't know who it was sent to.
378
	$db->query('', '
379
		DELETE FROM {db_prefix}pm_recipients
380
		WHERE id_member IN ({array_int:users})',
381
		array(
382
			'users' => $users,
383
		)
384
	);
385
386
	// Delete avatar.
387
	require_once(SUBSDIR . '/ManageAttachments.subs.php');
388
	removeAttachments(array('id_member' => $users));
389
390
	// It's over, no more moderation for you.
391
	$db->query('', '
392
		DELETE FROM {db_prefix}moderators
393
		WHERE id_member IN ({array_int:users})',
394
		array(
395
			'users' => $users,
396
		)
397
	);
398
	$db->query('', '
399
		DELETE FROM {db_prefix}group_moderators
400
		WHERE id_member IN ({array_int:users})',
401
		array(
402
			'users' => $users,
403
		)
404
	);
405
406
	// If you don't exist we can't ban you.
407
	$db->query('', '
408
		DELETE FROM {db_prefix}ban_items
409
		WHERE id_member IN ({array_int:users})',
410
		array(
411
			'users' => $users,
412
		)
413
	);
414
415
	// Remove individual theme settings.
416
	$db->query('', '
417
		DELETE FROM {db_prefix}themes
418
		WHERE id_member IN ({array_int:users})',
419
		array(
420
			'users' => $users,
421
		)
422
	);
423
424
	// These users are nobody's buddy nomore.
425
	$db->fetchQueryCallback('
426
		SELECT id_member, pm_ignore_list, buddy_list
427
		FROM {db_prefix}members
428
		WHERE FIND_IN_SET({raw:pm_ignore_list}, pm_ignore_list) != 0 OR FIND_IN_SET({raw:buddy_list}, buddy_list) != 0',
429
		array(
430
			'pm_ignore_list' => implode(', pm_ignore_list) != 0 OR FIND_IN_SET(', $users),
431
			'buddy_list' => implode(', buddy_list) != 0 OR FIND_IN_SET(', $users),
432
		),
433
		function ($row) use ($users)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

433
		/** @scrutinizer ignore-type */ function ($row) use ($users)
Loading history...
434
		{
435
			updateMemberData($row['id_member'], array(
436
				'pm_ignore_list' => implode(',', array_diff(explode(',', $row['pm_ignore_list']), $users)),
437
				'buddy_list' => implode(',', array_diff(explode(',', $row['buddy_list']), $users))
438
			));
439
		}
440
	);
441
442
	// Make sure no member's birthday is still sticking in the calendar...
443
	updateSettings(array(
444
		'calendar_updated' => time(),
445
	));
446
447
	// Integration rocks!
448
	call_integration_hook('integrate_delete_members', array($users));
449
450
	updateMemberStats();
451
452
	logActions($log_changes);
453
}
454
455
/**
456
 * Registers a member to the forum.
457
 *
458
 * What it does:
459
 *
460
 * - Allows two types of interface: 'guest' and 'admin'. The first
461
 * - includes hammering protection, the latter can perform the registration silently.
462
 * - The strings used in the options array are assumed to be escaped.
463
 * - Allows to perform several checks on the input, e.g. reserved names.
464
 * - The function will adjust member statistics.
465
 * - If an error is detected will fatal error on all errors unless return_errors is true.
466
 *
467
 * @package Members
468
 * @uses Auth.subs.php
469
 * @uses Mail.subs.php
470
 *
471
 * @param mixed[] $regOptions
472
 * @param string  $ErrorContext
473
 *
474
 * @return int the ID of the newly created member
475
 * @throws Elk_Exception no_theme
476
 */
477
function registerMember(&$regOptions, $ErrorContext = 'register')
478
{
479
	global $scripturl, $txt;
480
481
	$db = database();
482
483
	loadLanguage('Login');
484
485
	// We'll need some external functions.
486
	require_once(SUBSDIR . '/Auth.subs.php');
487
	require_once(SUBSDIR . '/Mail.subs.php');
488
489
	// Put any errors in here.
490
	$reg_errors = ElkArte\Errors\ErrorContext::context($ErrorContext, 0);
491
492
	// What method of authorization are we going to use?
493
	if (empty($regOptions['auth_method']) || !in_array($regOptions['auth_method'], array('password', 'openid')))
494
	{
495
		if (!empty($regOptions['openid']))
496
			$regOptions['auth_method'] = 'openid';
497
		else
498
			$regOptions['auth_method'] = 'password';
499
	}
500
501
	// Spaces and other odd characters are evil...
502
	$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']));
503
504
	// Valid emails only
505
	if (!Data_Validator::is_valid($regOptions, array('email' => 'valid_email|required|max_length[255]'), array('email' => 'trim')))
506
		$reg_errors->addError('bad_email');
507
508
	validateUsername(0, $regOptions['username'], $ErrorContext, !empty($regOptions['check_reserved_name']));
509
510
	// Generate a validation code if it's supposed to be emailed.
511
	$validation_code = $regOptions['require'] === 'activation' ? generateValidationCode(14) : '';
512
513
	// Does the first password match the second?
514
	if ($regOptions['password'] != $regOptions['password_check'] && $regOptions['auth_method'] == 'password')
515
		$reg_errors->addError('passwords_dont_match');
516
517
	// That's kind of easy to guess...
518
	if ($regOptions['password'] == '')
519
	{
520
		if ($regOptions['auth_method'] == 'password')
521
			$reg_errors->addError('no_password');
522
		else
523
			$regOptions['password'] = sha1(mt_rand());
524
	}
525
526
	// Now perform hard password validation as required.
527
	if (!empty($regOptions['check_password_strength']) && $regOptions['password'] != '')
528
	{
529
		$passwordError = validatePassword($regOptions['password'], $regOptions['username'], array($regOptions['email']));
530
531
		// Password isn't legal?
532
		if ($passwordError !== null)
0 ignored issues
show
introduced by
The condition $passwordError !== null is always true.
Loading history...
533
			$reg_errors->addError('profile_error_password_' . $passwordError);
534
	}
535
536
	// @todo move to controller
537
	// You may not be allowed to register this email.
538
	if (!empty($regOptions['check_email_ban']))
539
		isBannedEmail($regOptions['email'], 'cannot_register', $txt['ban_register_prohibited']);
540
541
	// Check if the email address is in use.
542
	if (userByEmail($regOptions['email'], $regOptions['username']))
543
	{
544
		$reg_errors->addError(array('email_in_use', array(htmlspecialchars($regOptions['email'], ENT_COMPAT, 'UTF-8'))));
545
	}
546
547
	// Perhaps someone else wants to check this user
548
	call_integration_hook('integrate_register_check', array(&$regOptions, &$reg_errors));
549
550
	// If there's any errors left return them at once!
551
	if ($reg_errors->hasErrors())
552
		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...
553
554
	$reservedVars = array(
555
		'actual_theme_url',
556
		'actual_images_url',
557
		'base_theme_dir',
558
		'base_theme_url',
559
		'default_images_url',
560
		'default_theme_dir',
561
		'default_theme_url',
562
		'default_template',
563
		'images_url',
564
		'number_recent_posts',
565
		'smiley_sets_default',
566
		'theme_dir',
567
		'theme_id',
568
		'theme_layers',
569
		'theme_templates',
570
		'theme_url',
571
	);
572
573
	// Can't change reserved vars.
574
	if (isset($regOptions['theme_vars']) && count(array_intersect(array_keys($regOptions['theme_vars']), $reservedVars)) != 0)
575
		throw new Elk_Exception('no_theme');
576
577
	$tokenizer = new Token_Hash();
578
579
	// @since 1.0.7 - This is necessary because validateLoginPassword
580
	// uses a pass-by-ref and would convert to hash $regOptions['password']
581
	// making it impossible to send the reminder email and even integrate
582
	// the registration
583
	$password = $regOptions['password'];
584
585
	// Some of these might be overwritten. (the lower ones that are in the arrays below.)
586
	$regOptions['register_vars'] = array(
587
		'member_name' => $regOptions['username'],
588
		'email_address' => $regOptions['email'],
589
		'passwd' => validateLoginPassword($password, '', $regOptions['username'], true),
590
		'password_salt' => $tokenizer->generate_hash(16),
591
		'posts' => 0,
592
		'date_registered' => !empty($regOptions['time']) ? $regOptions['time'] : time(),
593
		'member_ip' => $regOptions['interface'] == 'admin' ? '127.0.0.1' : $regOptions['ip'],
594
		'member_ip2' => $regOptions['interface'] == 'admin' ? '127.0.0.1' : $regOptions['ip2'],
595
		'validation_code' => substr(hash('sha256', $validation_code), 0, 10),
596
		'real_name' => !empty($regOptions['real_name']) ? $regOptions['real_name'] : $regOptions['username'],
597
		'pm_email_notify' => 1,
598
		'id_theme' => 0,
599
		'id_post_group' => 4,
600
		'lngfile' => '',
601
		'buddy_list' => '',
602
		'pm_ignore_list' => '',
603
		'message_labels' => '',
604
		'website_title' => '',
605
		'website_url' => '',
606
		'time_format' => '',
607
		'signature' => '',
608
		'avatar' => '',
609
		'usertitle' => '',
610
		'secret_question' => '',
611
		'secret_answer' => '',
612
		'additional_groups' => '',
613
		'ignore_boards' => '',
614
		'smiley_set' => '',
615
		'openid_uri' => (!empty($regOptions['openid']) ? $regOptions['openid'] : ''),
616
		'notify_announcements' => (!empty($regOptions['notify_announcements']) ? 1 : 0),
617
	);
618
619
	// Setup the activation status on this new account so it is correct - firstly is it an under age account?
620
	if ($regOptions['require'] == 'coppa')
621
	{
622
		$regOptions['register_vars']['is_activated'] = 5;
623
		// @todo This should be changed.  To what should be it be changed??
624
		$regOptions['register_vars']['validation_code'] = '';
625
	}
626
	// Maybe it can be activated right away?
627
	elseif ($regOptions['require'] == 'nothing')
628
		$regOptions['register_vars']['is_activated'] = 1;
629
	// Maybe it must be activated by email?
630
	elseif ($regOptions['require'] == 'activation')
631
		$regOptions['register_vars']['is_activated'] = 0;
632
	// Otherwise it must be awaiting approval!
633
	else
634
		$regOptions['register_vars']['is_activated'] = 3;
635
636
	if (isset($regOptions['memberGroup']))
637
	{
638
		require_once(SUBSDIR . '/Membergroups.subs.php');
639
640
		// Make sure the id_group will be valid, if this is an administrator.
641
		$regOptions['register_vars']['id_group'] = $regOptions['memberGroup'] == 1 && !allowedTo('admin_forum') ? 0 : $regOptions['memberGroup'];
642
643
		// Check if this group is assignable.
644
		$unassignableGroups = getUnassignableGroups(allowedTo('admin_forum'));
645
646
		if (in_array($regOptions['register_vars']['id_group'], $unassignableGroups))
647
			$regOptions['register_vars']['id_group'] = 0;
648
	}
649
650
	// Integrate optional member settings to be set.
651
	if (!empty($regOptions['extra_register_vars']))
652
		foreach ($regOptions['extra_register_vars'] as $var => $value)
653
			$regOptions['register_vars'][$var] = $value;
654
655
	// Integrate optional user theme options to be set.
656
	$theme_vars = array();
657
	if (!empty($regOptions['theme_vars']))
658
		foreach ($regOptions['theme_vars'] as $var => $value)
659
			$theme_vars[$var] = $value;
660
661
	// Right, now let's prepare for insertion.
662
	$knownInts = array(
663
		'date_registered', 'posts', 'id_group', 'last_login', 'personal_messages', 'unread_messages', 'notifications',
664
		'new_pm', 'pm_prefs', 'hide_email', 'show_online', 'pm_email_notify', 'karma_good', 'karma_bad',
665
		'notify_announcements', 'notify_send_body', 'notify_regularity', 'notify_types',
666
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
667
	);
668
	$knownFloats = array(
669
		'time_offset',
670
	);
671
672
	// Call an optional function to validate the users' input.
673
	call_integration_hook('integrate_register', array(&$regOptions, &$theme_vars, &$knownInts, &$knownFloats));
674
675
	$column_names = array();
676
	$values = array();
677
	foreach ($regOptions['register_vars'] as $var => $val)
678
	{
679
		$type = 'string';
680
		if (in_array($var, $knownInts))
681
			$type = 'int';
682
		elseif (in_array($var, $knownFloats))
683
			$type = 'float';
684
		elseif ($var == 'birthdate')
685
			$type = 'date';
686
687
		$column_names[$var] = $type;
688
		$values[$var] = $val;
689
	}
690
691
	// Register them into the database.
692
	$db->insert('',
693
		'{db_prefix}members',
694
		$column_names,
695
		$values,
696
		array('id_member')
697
	);
698
	$memberID = $db->insert_id('{db_prefix}members', 'id_member');
699
700
	// Update the number of members and latest member's info - and pass the name, but remove the 's.
701
	if ($regOptions['register_vars']['is_activated'] == 1)
702
		updateMemberStats($memberID, $regOptions['register_vars']['real_name']);
703
	else
704
		updateMemberStats();
705
706
	// @todo there's got to be a method that does this
707
	// Theme variables too?
708
	if (!empty($theme_vars))
709
	{
710
		$inserts = array();
711
		foreach ($theme_vars as $var => $val)
712
			$inserts[] = array($memberID, $var, $val);
713
		$db->insert('insert',
714
			'{db_prefix}themes',
715
			array('id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'),
716
			$inserts,
717
			array('id_member', 'variable')
718
		);
719
	}
720
721
	// If it's enabled, increase the registrations for today.
722
	trackStats(array('registers' => '+'));
723
724
	// @todo emails should be sent from the controller, with a new method.
725
726
	// Don't worry about what the emails might want to replace. Just give them everything and let them sort it out.
727
	$replacements = array(
728
		'REALNAME' => $regOptions['register_vars']['real_name'],
729
		'USERNAME' => $regOptions['username'],
730
		'PASSWORD' => $regOptions['password'],
731
		'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder',
732
		'ACTIVATIONLINK' => $scripturl . '?action=register;sa=activate;u=' . $memberID . ';code=' . $validation_code,
733
		'ACTIVATIONLINKWITHOUTCODE' => $scripturl . '?action=register;sa=activate;u=' . $memberID,
734
		'ACTIVATIONCODE' => $validation_code,
735
		'OPENID' => !empty($regOptions['openid']) ? $regOptions['openid'] : '',
736
		'COPPALINK' => $scripturl . '?action=register;sa=coppa;u=' . $memberID,
737
	);
738
739
	// Administrative registrations are a bit different...
740
	if ($regOptions['interface'] == 'admin')
741
	{
742
		if ($regOptions['require'] == 'activation')
743
			$email_message = 'admin_register_activate';
744
		elseif (!empty($regOptions['send_welcome_email']))
745
			$email_message = 'admin_register_immediate';
746
747
		if (isset($email_message))
748
		{
749
			$emaildata = loadEmailTemplate($email_message, $replacements);
750
751
			sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
752
		}
753
	}
754
	else
755
	{
756
		// Can post straight away - welcome them to your fantastic community...
757
		if ($regOptions['require'] == 'nothing')
758
		{
759
			if (!empty($regOptions['send_welcome_email']))
760
			{
761
				$replacements = array(
762
					'REALNAME' => $regOptions['register_vars']['real_name'],
763
					'USERNAME' => $regOptions['username'],
764
					'PASSWORD' => $regOptions['password'],
765
					'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder',
766
					'OPENID' => !empty($regOptions['openid']) ? $regOptions['openid'] : '',
767
				);
768
				$emaildata = loadEmailTemplate('register_' . ($regOptions['auth_method'] == 'openid' ? 'openid_' : '') . 'immediate', $replacements);
769
				$mark_down = new Html_2_Md(str_replace("\n", '<br>', $emaildata['body']));
770
				$emaildata['body'] = $mark_down->get_markdown();
771
772
				sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
773
			}
774
775
			// Send admin their notification.
776
			require_once(SUBSDIR . '/Notification.subs.php');
777
			sendAdminNotifications('standard', $memberID, $regOptions['username']);
778
		}
779
		// Need to activate their account - or fall under COPPA.
780
		elseif ($regOptions['require'] == 'activation' || $regOptions['require'] == 'coppa')
781
		{
782
783
			$emaildata = loadEmailTemplate('register_' . ($regOptions['auth_method'] == 'openid' ? 'openid_' : '') . ($regOptions['require'] == 'activation' ? 'activate' : 'coppa'), $replacements);
784
			$mark_down = new Html_2_Md(str_replace("\n", '<br>', $emaildata['body']));
785
			$emaildata['body'] = $mark_down->get_markdown();
786
787
			sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
788
		}
789
		// Must be awaiting approval.
790
		else
791
		{
792
			$replacements = array(
793
				'REALNAME' => $regOptions['register_vars']['real_name'],
794
				'USERNAME' => $regOptions['username'],
795
				'PASSWORD' => $regOptions['password'],
796
				'FORGOTPASSWORDLINK' => $scripturl . '?action=reminder',
797
				'OPENID' => !empty($regOptions['openid']) ? $regOptions['openid'] : '',
798
			);
799
800
			$emaildata = loadEmailTemplate('register_' . ($regOptions['auth_method'] == 'openid' ? 'openid_' : '') . 'pending', $replacements);
801
			$mark_down = new Html_2_Md(str_replace("\n", '<br>', $emaildata['body']));
802
			$emaildata['body'] = $mark_down->get_markdown();
803
804
			sendmail($regOptions['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
805
806
			// Admin gets informed here...
807
			require_once(SUBSDIR . '/Notification.subs.php');
808
			sendAdminNotifications('approval', $memberID, $regOptions['username']);
809
		}
810
811
		// Okay, they're for sure registered... make sure the session is aware of this for security. (Just married :P!)
812
		$_SESSION['just_registered'] = 1;
813
	}
814
815
	// If they are for sure registered, let other people to know about it
816
	call_integration_hook('integrate_register_after', array($regOptions, $memberID));
817
818
	return $memberID;
819
}
820
821
/**
822
 * Check if a name is in the reserved words list. (name, current member id, name/username?.)
823
 *
824
 * - checks if name is a reserved name or username.
825
 * - if is_name is false, the name is assumed to be a username.
826
 * - the id_member variable is used to ignore duplicate matches with the current member.
827
 *
828
 * @package Members
829
 *
830
 * @param string $name
831
 * @param int    $current_ID_MEMBER
832
 * @param bool   $is_name
833
 * @param bool   $fatal
834
 *
835
 * @return bool
836
 * @throws Elk_Exception username_reserved, name_censored
837
 */
838
function isReservedName($name, $current_ID_MEMBER = 0, $is_name = true, $fatal = true)
839
{
840
	global $modSettings;
841
842
	$db = database();
843
844
	$name = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'replaceEntities__callback', $name);
845
	$checkName = Util::strtolower($name);
846
847
	// Administrators are never restricted ;).
848
	if (!allowedTo('admin_forum') && ((!empty($modSettings['reserveName']) && $is_name) || !empty($modSettings['reserveUser']) && !$is_name))
849
	{
850
		$reservedNames = explode("\n", $modSettings['reserveNames']);
851
		// Case sensitive check?
852
		$checkMe = empty($modSettings['reserveCase']) ? $checkName : $name;
853
854
		// Check each name in the list...
855
		foreach ($reservedNames as $reserved)
856
		{
857
			if ($reserved == '')
858
				continue;
859
860
			// The admin might've used entities too, level the playing field.
861
			$reservedCheck = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'replaceEntities__callback', $reserved);
862
863
			// Case sensitive name?
864
			if (empty($modSettings['reserveCase']))
865
				$reservedCheck = Util::strtolower($reservedCheck);
866
867
			// If it's not just entire word, check for it in there somewhere...
868
			if ($checkMe == $reservedCheck || (Util::strpos($checkMe, $reservedCheck) !== false && empty($modSettings['reserveWord'])))
869
				if ($fatal)
870
					throw new Elk_Exception('username_reserved', 'password', array($reserved));
871
				else
872
					return true;
873
		}
874
875
		$censor_name = $name;
876
		if (censor($censor_name) != $name)
877
			if ($fatal)
878
				throw new Elk_Exception('name_censored', 'password', array($name));
879
			else
880
				return true;
881
	}
882
883
	// Characters we just shouldn't allow, regardless.
884
	foreach (array('*') as $char)
885
		if (strpos($checkName, $char) !== false)
886
			if ($fatal)
887
				throw new Elk_Exception('username_reserved', 'password', array($char));
888
			else
889
				return true;
890
891
	// Get rid of any SQL parts of the reserved name...
892
	$checkName = strtr($name, array('_' => '\\_', '%' => '\\%'));
893
894
	// Make sure they don't want someone else's name.
895
	$request = $db->query('', '
896
		SELECT id_member
897
		FROM {db_prefix}members
898
		WHERE ' . (empty($current_ID_MEMBER) ? '' : 'id_member != {int:current_member}
899
			AND ') . '({raw:real_name} LIKE {string:check_name} OR {raw:member_name} LIKE {string:check_name})
900
		LIMIT 1',
901
		array(
902
			'real_name' => defined('DB_CASE_SENSITIVE') ? 'LOWER(real_name)' : 'real_name',
903
			'member_name' => defined('DB_CASE_SENSITIVE') ? 'LOWER(member_name)' : 'member_name',
904
			'current_member' => $current_ID_MEMBER,
905
			'check_name' => $checkName,
906
		)
907
	);
908
	if ($db->num_rows($request) > 0)
909
	{
910
		$db->free_result($request);
911
		return true;
912
	}
913
914
	// Does name case insensitive match a member group name?
915
	$request = $db->query('', '
916
		SELECT id_group
917
		FROM {db_prefix}membergroups
918
		WHERE {raw:group_name} LIKE {string:check_name}
919
		LIMIT 1',
920
		array(
921
			'group_name' => defined('DB_CASE_SENSITIVE') ? 'LOWER(group_name)' : 'group_name',
922
			'check_name' => $checkName,
923
		)
924
	);
925
	if ($db->num_rows($request) > 0)
926
	{
927
		$db->free_result($request);
928
		return true;
929
	}
930
931
	// Okay, they passed.
932
	return false;
933
}
934
935
/**
936
 * Retrieves a list of membergroups that are allowed to do the given
937
 * permission. (on the given board)
938
 *
939
 * - If board_id is not null, a board permission is assumed.
940
 * - The function takes different permission settings into account.
941
 *
942
 * @package Members
943
 *
944
 * @param string       $permission
945
 * @param integer|null $board_id = null
946
 *
947
 * @return array containing an array for the allowed membergroup ID's
948
 * and an array for the denied membergroup ID's.
949
 * @throws Elk_Exception no_board
950
 */
951
function groupsAllowedTo($permission, $board_id = null)
952
{
953
	global $board_info;
954
955
	$db = database();
956
957
	// Admins are allowed to do anything.
958
	$member_groups = array(
959
		'allowed' => array(1),
960
		'denied' => array(),
961
	);
962
963
	// Assume we're dealing with regular permissions (like profile_view_own).
964
	if ($board_id === null)
965
	{
966
		$request = $db->query('', '
967
			SELECT id_group, add_deny
968
			FROM {db_prefix}permissions
969
			WHERE permission = {string:permission}',
970
			array(
971
				'permission' => $permission,
972
			)
973
		);
974
		while ($row = $db->fetch_assoc($request))
975
			$member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = $row['id_group'];
976
		$db->free_result($request);
977
	}
978
979
	// Otherwise it's time to look at the board.
980
	else
981
	{
982
		// First get the profile of the given board.
983
		if (isset($board_info['id']) && $board_info['id'] == $board_id)
984
			$profile_id = $board_info['profile'];
985
		elseif ($board_id !== 0)
986
		{
987
			require_once(SUBSDIR . '/Boards.subs.php');
988
			$board_data = fetchBoardsInfo(array('boards' => $board_id), array('selects' => 'permissions'));
989
990
			if (empty($board_data))
991
				throw new Elk_Exception('no_board');
992
			$profile_id = $board_data[$board_id]['id_profile'];
993
		}
994
		else
995
			$profile_id = 1;
996
997
		$request = $db->query('', '
998
			SELECT bp.id_group, bp.add_deny
999
			FROM {db_prefix}board_permissions AS bp
1000
			WHERE bp.permission = {string:permission}
1001
				AND bp.id_profile = {int:profile_id}',
1002
			array(
1003
				'profile_id' => $profile_id,
1004
				'permission' => $permission,
1005
			)
1006
		);
1007
		while ($row = $db->fetch_assoc($request))
1008
			$member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = $row['id_group'];
1009
		$db->free_result($request);
1010
	}
1011
1012
	// Denied is never allowed.
1013
	$member_groups['allowed'] = array_diff($member_groups['allowed'], $member_groups['denied']);
1014
1015
	return $member_groups;
1016
}
1017
1018
/**
1019
 * Retrieves a list of members that have a given permission (on a given board).
1020
 *
1021
 * - If board_id is not null, a board permission is assumed.
1022
 * - Takes different permission settings into account.
1023
 * - Takes possible moderators (on board 'board_id') into account.
1024
 *
1025
 * @package Members
1026
 * @param string $permission
1027
 * @param integer|null $board_id = null
1028
 *
1029
 * @return int[] an array containing member ID's.
1030
 * @throws Elk_Exception
1031
 */
1032
function membersAllowedTo($permission, $board_id = null)
1033
{
1034
	$db = database();
1035
1036
	$member_groups = groupsAllowedTo($permission, $board_id);
1037
1038
	$include_moderators = in_array(3, $member_groups['allowed']) && $board_id !== null;
1039
	$member_groups['allowed'] = array_diff($member_groups['allowed'], array(3));
1040
1041
	$exclude_moderators = in_array(3, $member_groups['denied']) && $board_id !== null;
1042
	$member_groups['denied'] = array_diff($member_groups['denied'], array(3));
1043
1044
	return $db->fetchQueryCallback('
1045
		SELECT mem.id_member
1046
		FROM {db_prefix}members AS mem' . ($include_moderators || $exclude_moderators ? '
1047
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_member = mem.id_member AND mods.id_board = {int:board_id})' : '') . '
1048
		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']) ? '' : '
1049
			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}))'),
1050
		array(
1051
			'member_groups_allowed' => $member_groups['allowed'],
1052
			'member_groups_denied' => $member_groups['denied'],
1053
			'board_id' => $board_id,
1054
			'member_group_allowed_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['allowed']),
1055
			'member_group_denied_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['denied']),
1056
		),
1057
		function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

1057
		/** @scrutinizer ignore-type */ function ($row)
Loading history...
1058
		{
1059
			return $row['id_member'];
1060
		}
1061
	);
1062
}
1063
1064
/**
1065
 * This function is used to re-associate members with relevant posts.
1066
 *
1067
 * - Re-attribute guest posts to a specified member.
1068
 * - Does not check for any permissions.
1069
 * - If add_to_post_count is set, the member's post count is increased.
1070
 *
1071
 * @package Members
1072
 *
1073
 * @param int $memID
1074
 * @param bool|false|string $email = false
1075
 * @param bool|false|string $membername = false
1076
 * @param bool $post_count = false
1077
 */
1078
function reattributePosts($memID, $email = false, $membername = false, $post_count = false)
1079
{
1080
	$db = database();
1081
1082
	// Firstly, if email and username aren't passed find out the members email address and name.
1083
	if ($email === false && $membername === false)
1084
	{
1085
		require_once(SUBSDIR . '/Members.subs.php');
1086
		$result = getBasicMemberData($memID);
1087
		$email = $result['email_address'];
1088
		$membername = $result['member_name'];
1089
	}
1090
1091
	// If they want the post count restored then we need to do some research.
1092
	if ($post_count)
1093
	{
1094
		$request = $db->query('', '
1095
			SELECT COUNT(*)
1096
			FROM {db_prefix}messages AS m
1097
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND b.count_posts = {int:count_posts})
1098
			WHERE m.id_member = {int:guest_id}
1099
				AND m.approved = {int:is_approved}
1100
				AND m.icon != {string:recycled_icon}' . (empty($email) ? '' : '
1101
				AND m.poster_email = {string:email_address}') . (empty($membername) ? '' : '
1102
				AND m.poster_name = {string:member_name}'),
1103
			array(
1104
				'count_posts' => 0,
1105
				'guest_id' => 0,
1106
				'email_address' => $email,
1107
				'member_name' => $membername,
1108
				'is_approved' => 1,
1109
				'recycled_icon' => 'recycled',
1110
			)
1111
		);
1112
		list ($messageCount) = $db->fetch_row($request);
1113
		$db->free_result($request);
1114
1115
		updateMemberData($memID, array('posts' => 'posts + ' . $messageCount));
1116
	}
1117
1118
	$query_parts = array();
1119
	if (!empty($email))
1120
		$query_parts[] = 'poster_email = {string:email_address}';
1121
	if (!empty($membername))
1122
		$query_parts[] = 'poster_name = {string:member_name}';
1123
	$query = implode(' AND ', $query_parts);
1124
1125
	// Finally, update the posts themselves!
1126
	$db->query('', '
1127
		UPDATE {db_prefix}messages
1128
		SET id_member = {int:memID}
1129
		WHERE ' . $query,
1130
		array(
1131
			'memID' => $memID,
1132
			'email_address' => $email,
1133
			'member_name' => $membername,
1134
		)
1135
	);
1136
1137
	// ...and the topics too!
1138
	$db->query('', '
1139
		UPDATE {db_prefix}topics as t, {db_prefix}messages as m
1140
		SET t.id_member_started = {int:memID}
1141
		WHERE m.id_member = {int:memID}
1142
			AND t.id_first_msg = m.id_msg',
1143
		array(
1144
			'memID' => $memID,
1145
		)
1146
	);
1147
1148
	// Allow mods with their own post tables to re-attribute posts as well :)
1149
	call_integration_hook('integrate_reattribute_posts', array($memID, $email, $membername, $post_count));
1150
}
1151
1152
/**
1153
 * Gets a listing of members, Callback for createList().
1154
 *
1155
 * @package Members
1156
 * @param int $start The item to start with (for pagination purposes)
1157
 * @param int $items_per_page  The number of items to show per page
1158
 * @param string $sort A string indicating how to sort the results
1159
 * @param string $where
1160
 * @param mixed[] $where_params
1161
 * @param boolean $get_duplicates
1162
 */
1163
function list_getMembers($start, $items_per_page, $sort, $where, $where_params = array(), $get_duplicates = false)
1164
{
1165
	$db = database();
1166
1167
	$members = $db->fetchQuery('
1168
		SELECT
1169
			mem.id_member, mem.member_name, mem.real_name, mem.email_address, mem.member_ip, mem.member_ip2, mem.last_login,
1170
			mem.posts, mem.is_activated, mem.date_registered, mem.id_group, mem.additional_groups, mg.group_name
1171
		FROM {db_prefix}members AS mem
1172
			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group)
1173
		WHERE ' . ($where == '1' ? '1=1' : $where) . '
1174
		ORDER BY {raw:sort}
1175
		LIMIT {int:start}, {int:per_page}',
1176
		array_merge($where_params, array(
1177
			'sort' => $sort,
1178
			'start' => $start,
1179
			'per_page' => $items_per_page,
1180
		))
1181
	);
1182
1183
	// If we want duplicates pass the members array off.
1184
	if ($get_duplicates)
1185
		populateDuplicateMembers($members);
1186
1187
	return $members;
1188
}
1189
1190
/**
1191
 * Gets the number of members, Callback for createList().
1192
 *
1193
 * @package Members
1194
 * @param string $where
1195
 * @param mixed[] $where_params
1196
 */
1197
function list_getNumMembers($where, $where_params = array())
1198
{
1199
	global $modSettings;
1200
1201
	$db = database();
1202
1203
	// We know how many members there are in total.
1204
	if (empty($where) || $where == '1=1')
1205
		$num_members = $modSettings['totalMembers'];
1206
1207
	// The database knows the amount when there are extra conditions.
1208
	else
1209
	{
1210
		$request = $db->query('', '
1211
			SELECT COUNT(*)
1212
			FROM {db_prefix}members AS mem
1213
			WHERE ' . $where,
1214
			array_merge($where_params, array(
1215
			))
1216
		);
1217
		list ($num_members) = $db->fetch_row($request);
1218
		$db->free_result($request);
1219
	}
1220
1221
	return $num_members;
1222
}
1223
1224
/**
1225
 * Find potential duplicate registration members based on the same IP address
1226
 *
1227
 * @package Members
1228
 * @param mixed[] $members
1229
 */
1230
function populateDuplicateMembers(&$members)
1231
{
1232
	$db = database();
1233
1234
	// This will hold all the ip addresses.
1235
	$ips = array();
1236
	foreach ($members as $key => $member)
1237
	{
1238
		// Create the duplicate_members element.
1239
		$members[$key]['duplicate_members'] = array();
1240
1241
		// Store the IPs.
1242
		if (!empty($member['member_ip']))
1243
			$ips[] = $member['member_ip'];
1244
		if (!empty($member['member_ip2']))
1245
			$ips[] = $member['member_ip2'];
1246
	}
1247
1248
	$ips = array_unique($ips);
1249
1250
	if (empty($ips))
1251
		return false;
1252
1253
	// Fetch all members with this IP address, we'll filter out the current ones in a sec.
1254
	$potential_dupes = membersByIP($ips, 'exact', true);
1255
1256
	$duplicate_members = array();
1257
	$duplicate_ids = array();
1258
	foreach ($potential_dupes as $row)
1259
	{
1260
		//$duplicate_ids[] = $row['id_member'];
1261
1262
		$member_context = array(
1263
			'id' => $row['id_member'],
1264
			'name' => $row['member_name'],
1265
			'email' => $row['email_address'],
1266
			'is_banned' => $row['is_activated'] > 10,
1267
			'ip' => $row['member_ip'],
1268
			'ip2' => $row['member_ip2'],
1269
		);
1270
1271
		if (in_array($row['member_ip'], $ips))
1272
			$duplicate_members[$row['member_ip']][] = $member_context;
1273
		if ($row['member_ip'] != $row['member_ip2'] && in_array($row['member_ip2'], $ips))
1274
			$duplicate_members[$row['member_ip2']][] = $member_context;
1275
	}
1276
1277
	// Also try to get a list of messages using these ips.
1278
	$request = $db->query('', '
1279
		SELECT
1280
			m.poster_ip, mem.id_member, mem.member_name, mem.email_address, mem.is_activated
1281
		FROM {db_prefix}messages AS m
1282
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1283
		WHERE m.id_member != 0
1284
			' . (!empty($duplicate_ids) ? 'AND m.id_member NOT IN ({array_int:duplicate_ids})' : '') . '
1285
			AND m.poster_ip IN ({array_string:ips})',
1286
		array(
1287
			'duplicate_ids' => $duplicate_ids,
1288
			'ips' => $ips,
1289
		)
1290
	);
1291
1292
	$had_ips = array();
1293
	while ($row = $db->fetch_assoc($request))
1294
	{
1295
		// Don't collect lots of the same.
1296
		if (isset($had_ips[$row['poster_ip']]) && in_array($row['id_member'], $had_ips[$row['poster_ip']]))
1297
			continue;
1298
		$had_ips[$row['poster_ip']][] = $row['id_member'];
1299
1300
		$duplicate_members[$row['poster_ip']][] = array(
1301
			'id' => $row['id_member'],
1302
			'name' => $row['member_name'],
1303
			'email' => $row['email_address'],
1304
			'is_banned' => $row['is_activated'] > 10,
1305
			'ip' => $row['poster_ip'],
1306
			'ip2' => $row['poster_ip'],
1307
		);
1308
	}
1309
	$db->free_result($request);
1310
1311
	// Now we have all the duplicate members, stick them with their respective member in the list.
1312
	if (!empty($duplicate_members))
1313
	{
1314
		foreach ($members as $key => $member)
1315
		{
1316
			if (isset($duplicate_members[$member['member_ip']]))
1317
				$members[$key]['duplicate_members'] = $duplicate_members[$member['member_ip']];
1318
			if ($member['member_ip'] != $member['member_ip2'] && isset($duplicate_members[$member['member_ip2']]))
1319
				$members[$key]['duplicate_members'] = array_merge($member['duplicate_members'], $duplicate_members[$member['member_ip2']]);
1320
1321
			// Check we don't have lots of the same member.
1322
			$member_track = array($member['id_member']);
1323
			foreach ($members[$key]['duplicate_members'] as $duplicate_id_member => $duplicate_member)
1324
			{
1325
				if (in_array($duplicate_member['id'], $member_track))
1326
				{
1327
					unset($members[$key]['duplicate_members'][$duplicate_id_member]);
1328
					continue;
1329
				}
1330
1331
				$member_track[] = $duplicate_member['id'];
1332
			}
1333
		}
1334
	}
1335
}
1336
1337
/**
1338
 * Find members with a given IP (first, second, exact or "relaxed")
1339
 *
1340
 * @package Members
1341
 * @param string|string[] $ip1 An IP or an array of IPs
1342
 * @param string $match (optional, default 'exact') if the match should be exact
1343
 *                of "relaxed" (using LIKE)
1344
 * @param bool $ip2 (optional, default false) If the query should check IP2 as well
1345
 */
1346
function membersByIP($ip1, $match = 'exact', $ip2 = false)
1347
{
1348
	$db = database();
1349
1350
	$ip_params = array('ips' => array());
1351
	$ip_query = array();
1352
	foreach (array($ip1, $ip2) as $id => $ip)
1353
	{
1354
		if ($ip === false)
1355
			continue;
1356
1357
		if ($match === 'exact')
1358
			$ip_params['ips'] = array_merge($ip_params['ips'], (array) $ip);
1359
		else
1360
		{
1361
			$ip = (array) $ip;
1362
			foreach ($ip as $id_var => $ip_var)
1363
			{
1364
				$ip_var = str_replace('*', '%', $ip_var);
1365
				$ip_query[] = strpos($ip_var, '%') === false ? '= {string:ip_address_' . $id . '_' . $id_var . '}' : 'LIKE {string:ip_address_' . $id . '_' . $id_var . '}';
1366
				$ip_params['ip_address_' . $id . '_' . $id_var] = $ip_var;
1367
			}
1368
		}
1369
	}
1370
1371
	if ($match === 'exact')
1372
	{
1373
		$where = 'member_ip IN ({array_string:ips})';
1374
		if ($ip2 !== false)
1375
			$where .= '
1376
			OR member_ip2 IN ({array_string:ips})';
1377
	}
1378
	else
1379
	{
1380
		$where = 'member_ip ' . implode(' OR member_ip', $ip_query);
1381
		if ($ip2 !== false)
1382
			$where .= '
1383
			OR member_ip2 ' . implode(' OR member_ip', $ip_query);
1384
	}
1385
1386
	return $db->fetchQuery('
1387
		SELECT
1388
			id_member, member_name, email_address, member_ip, member_ip2, is_activated
1389
		FROM {db_prefix}members
1390
		WHERE ' . $where,
1391
		$ip_params
1392
	);
1393
}
1394
1395
/**
1396
 * Find out if there is another admin than the given user.
1397
 *
1398
 * @package Members
1399
 * @param int $memberID ID of the member, to compare with.
1400
 */
1401
function isAnotherAdmin($memberID)
1402
{
1403
	$db = database();
1404
1405
	$request = $db->query('', '
1406
		SELECT id_member
1407
		FROM {db_prefix}members
1408
		WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1409
			AND id_member != {int:selected_member}
1410
		LIMIT 1',
1411
		array(
1412
			'admin_group' => 1,
1413
			'selected_member' => $memberID,
1414
		)
1415
	);
1416
	list ($another) = $db->fetch_row($request);
1417
	$db->free_result($request);
1418
1419
	return $another;
1420
}
1421
1422
/**
1423
 * This function retrieves a list of member ids based on a set of conditions
1424
 *
1425
 * @package Members
1426
 * @param mixed[]|string $query see prepareMembersByQuery
1427
 * @param mixed[] $query_params see prepareMembersByQuery
1428
 * @param bool $details if true returns additional member details (name, email, ip, etc.)
1429
 *             false will only return an array of member id's that match the conditions
1430
 * @param bool $only_active see prepareMembersByQuery
1431
 */
1432
function membersBy($query, $query_params, $details = false, $only_active = true)
1433
{
1434
	$db = database();
1435
1436
	$query_where = prepareMembersByQuery($query, $query_params, $only_active);
1437
1438
	// Lets see who we can find that meets the built up conditions
1439
	$members = array();
1440
	$request = $db->query('', '
1441
		SELECT id_member' . ($details ? ', member_name, real_name, email_address, member_ip, date_registered, last_login,
1442
				hide_email, posts, is_activated, real_name' : '') . '
1443
		FROM {db_prefix}members
1444
		WHERE ' . $query_where . (isset($query_params['start']) ? '
0 ignored issues
show
Bug introduced by
Are you sure $query_where of type false|mixed can be used in concatenation? ( Ignorable by Annotation )

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

1444
		WHERE ' . /** @scrutinizer ignore-type */ $query_where . (isset($query_params['start']) ? '
Loading history...
1445
		LIMIT {int:start}, {int:limit}' : '') . (!empty($query_params['order']) ? '
1446
		ORDER BY {raw:order}' : ''),
1447
		$query_params
1448
	);
1449
1450
	// Return all the details for each member found
1451
	if ($details)
1452
	{
1453
		while ($row = $db->fetch_assoc($request))
1454
			$members[$row['id_member']] = $row;
1455
	}
1456
	// Or just a int[] of found member id's
1457
	else
1458
	{
1459
		while ($row = $db->fetch_assoc($request))
1460
			$members[] = $row['id_member'];
1461
	}
1462
	$db->free_result($request);
1463
1464
	return $members;
1465
}
1466
1467
/**
1468
 * Counts the number of members based on conditions
1469
 *
1470
 * @package Members
1471
 * @param string[]|string $query see prepareMembersByQuery
1472
 * @param mixed[] $query_params see prepareMembersByQuery
1473
 * @param boolean $only_active see prepareMembersByQuery
1474
 */
1475
function countMembersBy($query, $query_params, $only_active = true)
1476
{
1477
	$db = database();
1478
1479
	$query_where = prepareMembersByQuery($query, $query_params, $only_active);
1480
1481
	$request = $db->query('', '
1482
		SELECT COUNT(*)
1483
		FROM {db_prefix}members
1484
		WHERE ' . $query_where,
0 ignored issues
show
Bug introduced by
Are you sure $query_where of type false|mixed can be used in concatenation? ( Ignorable by Annotation )

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

1484
		WHERE ' . /** @scrutinizer ignore-type */ $query_where,
Loading history...
1485
		$query_params
1486
	);
1487
1488
	list ($num_members) = $db->fetch_row($request);
1489
	$db->free_result($request);
1490
1491
	return $num_members;
1492
}
1493
1494
/**
1495
 * Builds the WHERE clause for the functions countMembersBy and membersBy
1496
 *
1497
 * @package Members
1498
 * @param mixed[]|string $query can be an array of "type" of conditions,
1499
 *             or a string used as raw query
1500
 *             or a string that represents one of the built-in conditions
1501
 *             like member_names, not_in_group, etc
1502
 * @param mixed[] $query_params is an array containing the parameters passed to the query
1503
 *             'start' and 'limit' used in LIMIT
1504
 *             'order' used raw in ORDER BY
1505
 *             others passed as query params
1506
 * @param bool $only_active only fetch active members
1507
 */
1508
function prepareMembersByQuery($query, &$query_params, $only_active = true)
1509
{
1510
	$allowed_conditions = array(
1511
		'member_ids'   => 'id_member IN ({array_int:member_ids})',
1512
		'member_names' => function (&$members)
1513
		{
1514
			$mem_query = array();
1515
1516
			foreach ($members['member_names'] as $key => $param)
1517
			{
1518
				$mem_query[] = (defined('DB_CASE_SENSITIVE') ? 'LOWER(real_name)' : 'real_name') . ' LIKE {string:member_names_' . $key . '}';
1519
				$members['member_names_' . $key] = defined('DB_CASE_SENSITIVE') ? strtolower($param) : $param;
1520
			}
1521
			return implode("\n\t\t\tOR ", $mem_query);
1522
		},
1523
		'not_in_group'     => '(id_group != {int:not_in_group} AND FIND_IN_SET({int:not_in_group}, additional_groups) = 0)',
1524
		'in_group'         => '(id_group = {int:in_group} OR FIND_IN_SET({int:in_group}, additional_groups) != 0)',
1525
		'in_group_primary' => 'id_group = {int:in_group_primary}',
1526
		'in_post_group'    => 'id_post_group = {int:in_post_group}',
1527
		'in_group_no_add'  => '(id_group = {int:in_group_no_add} AND FIND_IN_SET({int:in_group_no_add}, additional_groups) = 0)',
1528
	);
1529
1530
	// Are there multiple parts to this query
1531
	if (is_array($query))
1532
	{
1533
		$query_parts = array('or' => array(), 'and' => array());
1534
		foreach ($query as $type => $query_conditions)
1535
		{
1536
			if (is_array($query_conditions))
1537
			{
1538
				foreach ($query_conditions as $condition => $query_condition)
1539
				{
1540
					if ($query_condition == 'member_names')
1541
						$query_parts[$condition === 'or' ? 'or' : 'and'][] = $allowed_conditions[$query_condition]($query_params);
1542
					else
1543
						$query_parts[$condition === 'or' ? 'or' : 'and'][] = isset($allowed_conditions[$query_condition]) ? $allowed_conditions[$query_condition] : $query_condition;
1544
				}
1545
			}
1546
			elseif ($query_conditions == 'member_names')
1547
			{
1548
				$query_parts['and'][] = $allowed_conditions[$query_conditions]($query_params);
1549
			}
1550
			else
1551
			{
1552
				$query_parts['and'][] = isset($allowed_conditions[$query_conditions]) ? $allowed_conditions[$query_conditions] : $query_conditions;
1553
			}		}
1554
1555
		if (!empty($query_parts['or']))
1556
			$query_parts['and'][] = implode("\n\t\t\tOR ", $query_parts['or']);
1557
1558
		$query_where = implode("\n\t\t\tAND ", $query_parts['and']);
1559
	}
1560
	// Is it one of our predefined querys like member_ids, member_names, etc
1561
	elseif (isset($allowed_conditions[$query]))
1562
	{
1563
		if ($query == 'member_names')
1564
			$query_where = $allowed_conditions[$query]($query_params);
1565
		else
1566
			$query_where = $allowed_conditions[$query];
1567
	}
1568
	// Something else, be careful ;)
1569
	else
1570
		$query_where = $query;
1571
1572
	// Lazy loading, our favorite
1573
	if (empty($query_where))
1574
		return false;
1575
1576
	// Only want active members
1577
	if ($only_active)
1578
	{
1579
		$query_where .= '
1580
			AND is_activated = {int:is_activated}';
1581
		$query_params['is_activated'] = 1;
1582
	}
1583
1584
	return $query_where;
1585
}
1586
1587
/**
1588
 * Retrieve administrators of the site.
1589
 *
1590
 * - The function returns basic information: name, language file.
1591
 * - It is used in personal messages reporting.
1592
 *
1593
 * @package Members
1594
 * @param int $id_admin = 0 if requested, only data about a specific admin is retrieved
1595
 */
1596
function admins($id_admin = 0)
1597
{
1598
	$db = database();
1599
1600
	// Now let's get out and loop through the admins.
1601
	$request = $db->query('', '
1602
		SELECT id_member, real_name, lngfile
1603
		FROM {db_prefix}members
1604
		WHERE (id_group = {int:admin_group} OR FIND_IN_SET({int:admin_group}, additional_groups) != 0)
1605
			' . (empty($id_admin) ? '' : 'AND id_member = {int:specific_admin}') . '
1606
		ORDER BY real_name, lngfile',
1607
		array(
1608
			'admin_group' => 1,
1609
			'specific_admin' => isset($id_admin) ? (int) $id_admin : 0,
1610
		)
1611
	);
1612
1613
	$admins = array();
1614
	while ($row = $db->fetch_assoc($request))
1615
		$admins[$row['id_member']] = array($row['real_name'], $row['lngfile']);
1616
	$db->free_result($request);
1617
1618
	return $admins;
1619
}
1620
1621
/**
1622
 * Get the last known id_member
1623
 * @return int
1624
 */
1625
function maxMemberID()
1626
{
1627
	$db = database();
1628
1629
	$request = $db->query('', '
1630
		SELECT MAX(id_member)
1631
		FROM {db_prefix}members',
1632
		array(
1633
		)
1634
	);
1635
	list ($max_id) = $db->fetch_row($request);
1636
	$db->free_result($request);
1637
1638
	return $max_id;
1639
}
1640
1641
/**
1642
 * Load some basic member information
1643
 *
1644
 * @package Members
1645
 * @param int[]|int $member_ids an array of member IDs or a single ID
1646
 * @param mixed[] $options an array of possible little alternatives, can be:
1647
 * - 'add_guest' (bool) to add a guest user to the returned array
1648
 * - 'limit' int if set overrides the default query limit
1649
 * - 'sort' (string) a column to sort the results
1650
 * - 'moderation' (bool) includes member_ip, id_group, additional_groups, last_login
1651
 * - 'authentication' (bool) includes secret_answer, secret_question, openid_uri,
1652
 *    is_activated, validation_code, passwd_flood, password_salt
1653
 * - 'preferences' (bool) includes lngfile, mod_prefs, notify_types, signature
1654
 * @return array
1655
 */
1656
function getBasicMemberData($member_ids, $options = array())
1657
{
1658
	global $txt, $language;
1659
1660
	$db = database();
1661
1662
	$members = array();
1663
1664
	if (empty($member_ids))
1665
		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 array.
Loading history...
1666
1667
	if (!is_array($member_ids))
1668
	{
1669
		$single = true;
1670
		$member_ids = array($member_ids);
1671
	}
1672
1673
	if (!empty($options['add_guest']))
1674
	{
1675
		$single = false;
1676
		// This is a guest...
1677
		$members[0] = array(
1678
			'id_member' => 0,
1679
			'member_name' => '',
1680
			'real_name' => $txt['guest_title'],
1681
			'email_address' => '',
1682
		);
1683
	}
1684
1685
	// Get some additional member info...
1686
	$request = $db->query('', '
1687
		SELECT id_member, member_name, real_name, email_address, hide_email, posts, id_theme' . (!empty($options['moderation']) ? ',
1688
		member_ip, id_group, additional_groups, last_login, id_post_group' : '') . (!empty($options['authentication']) ? ',
1689
		secret_answer, secret_question, openid_uri, is_activated, validation_code, passwd_flood, password_salt' : '') . (!empty($options['preferences']) ? ',
1690
		lngfile, mod_prefs, notify_types, signature' : '') . '
1691
		FROM {db_prefix}members
1692
		WHERE id_member IN ({array_int:member_list})
1693
		' . (isset($options['sort']) ? '
1694
		ORDER BY {raw:sort}' : '') . '
1695
		LIMIT {int:limit}',
1696
		array(
1697
			'member_list' => $member_ids,
1698
			'limit' => isset($options['limit']) ? $options['limit'] : count($member_ids),
1699
			'sort' => isset($options['sort']) ? $options['sort'] : '',
1700
		)
1701
	);
1702
	while ($row = $db->fetch_assoc($request))
1703
	{
1704
		if (empty($row['lngfile']))
1705
			$row['lngfile'] = $language;
1706
1707
		if (!empty($single))
1708
			$members = $row;
1709
		else
1710
			$members[$row['id_member']] = $row;
1711
	}
1712
	$db->free_result($request);
1713
1714
	return $members;
1715
}
1716
1717
/**
1718
 * Counts all inactive members
1719
 *
1720
 * @package Members
1721
 * @return array $inactive_members
1722
 */
1723
function countInactiveMembers()
1724
{
1725
	$db = database();
1726
1727
	$inactive_members = array();
1728
1729
	$request = $db->query('', '
1730
		SELECT COUNT(*) AS total_members, is_activated
1731
		FROM {db_prefix}members
1732
		WHERE is_activated != {int:is_activated}
1733
		GROUP BY is_activated',
1734
		array(
1735
			'is_activated' => 1,
1736
		)
1737
	);
1738
1739
	while ($row = $db->fetch_assoc($request))
1740
		$inactive_members[$row['is_activated']] = $row['total_members'];
1741
	$db->free_result($request);
1742
1743
	return $inactive_members;
1744
}
1745
1746
/**
1747
 * Get the member's id and group
1748
 *
1749
 * @package Members
1750
 * @param string $name
1751
 * @param bool $flexible if true searches for both real_name and member_name (default false)
1752
 * @return integer
1753
 */
1754
function getMemberByName($name, $flexible = false)
1755
{
1756
	$db = database();
1757
1758
	$request = $db->query('', '
1759
		SELECT id_member, id_group
1760
		FROM {db_prefix}members
1761
		WHERE {raw:real_name} LIKE {string:name}' . ($flexible ? '
1762
			OR {raw:member_name} LIKE {string:name}' : '') . '
1763
		LIMIT 1',
1764
		array(
1765
			'name' => Util::strtolower($name),
1766
			'real_name' => defined('DB_CASE_SENSITIVE') ? 'LOWER(real_name)' : 'real_name',
1767
			'member_name' => defined('DB_CASE_SENSITIVE') ? 'LOWER(member_name)' : 'member_name',
1768
		)
1769
	);
1770
	if ($db->num_rows($request) == 0)
1771
		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...
1772
	$member = $db->fetch_assoc($request);
1773
	$db->free_result($request);
1774
1775
	return $member;
1776
}
1777
1778
/**
1779
 * Finds a member from the database using supplied string as real_name
1780
 *
1781
 * - Optionally will only search/find the member in a buddy list
1782
 *
1783
 * @package Members
1784
 * @param string $search string to search real_name for like finds
1785
 * @param int[]|null $buddies
1786
 */
1787
function getMember($search, $buddies = array())
1788
{
1789
	$db = database();
1790
1791
	$xml_data = array(
1792
		'items' => array(
1793
			'identifier' => 'item',
1794
			'children' => array(),
1795
		),
1796
	);
1797
	// Find the member.
1798
	$xml_data['items']['children'] = $db->fetchQueryCallback('
1799
		SELECT id_member, real_name
1800
		FROM {db_prefix}members
1801
		WHERE {raw:real_name} LIKE {string:search}' . (!empty($buddies) ? '
1802
			AND id_member IN ({array_int:buddy_list})' : '') . '
1803
			AND is_activated IN ({array_int:activation_status})
1804
		ORDER BY LENGTH(real_name), real_name
1805
		LIMIT {int:limit}',
1806
		array(
1807
			'real_name' => defined('DB_CASE_SENSITIVE') ? 'LOWER(real_name)' : 'real_name',
1808
			'buddy_list' => $buddies,
1809
			'search' => Util::strtolower($search),
1810
			'activation_status' => array(1, 12),
1811
			'limit' => Util::strlen($search) <= 2 ? 100 : 200,
1812
		),
1813
		function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

1813
		/** @scrutinizer ignore-type */ function ($row)
Loading history...
1814
		{
1815
			$row['real_name'] = strtr($row['real_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
1816
1817
			return array(
1818
				'attributes' => array(
1819
					'id' => $row['id_member'],
1820
				),
1821
				'value' => $row['real_name'],
1822
			);
1823
		}
1824
	);
1825
1826
	return $xml_data;
1827
}
1828
1829
/**
1830
 * Retrieves MemberData based on conditions
1831
 *
1832
 * @package Members
1833
 * @param mixed[] $conditions associative array holding the conditions for the WHERE clause of the query.
1834
 * Possible keys:
1835
 * - activated_status (boolean) must be present
1836
 * - time_before (integer)
1837
 * - members (array of integers)
1838
 * - member_greater (integer) a member id, it will be used to filter only members with id_member greater than this
1839
 * - group_list (array) array of group IDs
1840
 * - notify_announcements (integer)
1841
 * - order_by (string)
1842
 * - limit (int)
1843
 * @return array
1844
 */
1845
function retrieveMemberData($conditions)
1846
{
1847
	global $modSettings, $language;
1848
1849
	// We badly need this
1850
	assert(isset($conditions['activated_status']));
1851
1852
	$db = database();
1853
1854
	$available_conditions = array(
1855
		'time_before' => '
1856
				AND date_registered < {int:time_before}',
1857
		'members' => '
1858
				AND id_member IN ({array_int:members})',
1859
		'member_greater' => '
1860
				AND id_member > {int:member_greater}',
1861
		'member_greater_equal' => '
1862
				AND id_member >= {int:member_greater_equal}',
1863
		'member_lesser' => '
1864
				AND id_member < {int:member_lesser}',
1865
		'member_lesser_equal' => '
1866
				AND id_member <= {int:member_lesser_equal}',
1867
		'group_list' => '
1868
				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)',
1869
		'notify_announcements' => '
1870
				AND notify_announcements = {int:notify_announcements}'
1871
	);
1872
1873
	$query_cond = array();
1874
	foreach ($conditions as $key => $dummy)
1875
		if (isset($available_conditions[$key]))
1876
			$query_cond[] = $available_conditions[$key];
1877
1878
	if (isset($conditions['group_list']))
1879
		$conditions['additional_group_list'] = implode(', additional_groups) != 0 OR FIND_IN_SET(', $conditions['group_list']);
1880
1881
	$data = array();
1882
1883
	if (!isset($conditions['order_by']))
1884
		$conditions['order_by'] = 'lngfile';
1885
1886
	$limit = (isset($conditions['limit'])) ? '
1887
		LIMIT {int:limit}' : '';
1888
1889
	// Get information on each of the members, things that are important to us, like email address...
1890
	$request = $db->query('', '
1891
		SELECT id_member, member_name, real_name, email_address, validation_code, lngfile
1892
		FROM {db_prefix}members
1893
		WHERE is_activated = {int:activated_status}' . implode('', $query_cond) . '
1894
		ORDER BY {raw:order_by}' . $limit,
1895
		$conditions
1896
	);
1897
1898
	$data['member_count'] = $db->num_rows($request);
1899
1900
	if ($data['member_count'] == 0)
1901
		return $data;
1902
1903
	// Fill the info array.
1904
	while ($row = $db->fetch_assoc($request))
1905
	{
1906
		$data['members'][] = $row['id_member'];
1907
		$data['member_info'][] = array(
1908
			'id' => $row['id_member'],
1909
			'username' => $row['member_name'],
1910
			'name' => $row['real_name'],
1911
			'email' => $row['email_address'],
1912
			'language' => empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'],
1913
			'code' => $row['validation_code']
1914
		);
1915
	}
1916
	$db->free_result($request);
1917
1918
	return $data;
1919
}
1920
1921
/**
1922
 * Activate members
1923
 *
1924
 * @package Members
1925
 * @param mixed[] $conditions associative array holding the conditions for the WHERE clause of the query.
1926
 * Possible keys:
1927
 * - activated_status (boolean) must be present
1928
 * - time_before (integer)
1929
 * - members (array of integers)
1930
 */
1931
function approveMembers($conditions)
1932
{
1933
	$db = database();
1934
1935
	// This shall be present
1936
	assert(isset($conditions['activated_status']));
1937
1938
	$available_conditions = array(
1939
		'time_before' => '
1940
				AND date_registered < {int:time_before}',
1941
		'members' => '
1942
				AND id_member IN ({array_int:members})',
1943
	);
1944
1945
	// @todo maybe an hook here?
1946
	$query_cond = array();
1947
	$query = false;
1948
	foreach ($conditions as $key => $dummy)
1949
	{
1950
		if (isset($available_conditions[$key]))
1951
		{
1952
			if ($key === 'time_before')
1953
				$query = true;
1954
			$query_cond[] = $available_conditions[$key];
1955
		}
1956
	}
1957
1958
	if ($query)
1959
	{
1960
		$data = retrieveMemberData($conditions);
1961
		$members_id = array();
1962
		foreach ($data['member_info'] as $member)
1963
			$members_id[] = $member['username'];
1964
	}
1965
	else
1966
	{
1967
		$members_id = $conditions['members'];
1968
	}
1969
1970
	$conditions['is_activated'] = $conditions['activated_status'] >= 10 ? 11 : 1;
1971
	$conditions['blank_string'] = '';
1972
1973
	// Approve/activate this member.
1974
	$db->query('', '
1975
		UPDATE {db_prefix}members
1976
		SET validation_code = {string:blank_string}, is_activated = {int:is_activated}
1977
		WHERE is_activated = {int:activated_status}' . implode('', $query_cond),
1978
		$conditions
1979
	);
1980
1981
	// Let the integration know that they've been activated!
1982
	foreach ($members_id as $member_id)
1983
		call_integration_hook('integrate_activate', array($member_id, $conditions['activated_status'], $conditions['is_activated']));
1984
1985
	return $conditions['is_activated'];
1986
}
1987
1988
/**
1989
 * Set these members for activation
1990
 *
1991
 * @package Members
1992
 * @param mixed[] $conditions associative array holding the conditions for the  WHERE clause of the query.
1993
 * Possible keys:
1994
 * - selected_member (integer) must be present
1995
 * - activated_status (boolean) must be present
1996
 * - validation_code (string) must be present
1997
 * - members (array of integers)
1998
 * - time_before (integer)
1999
 */
2000
function enforceReactivation($conditions)
2001
{
2002
	$db = database();
2003
2004
	// We need all of these
2005
	assert(isset($conditions['activated_status']));
2006
	assert(isset($conditions['selected_member']));
2007
	assert(isset($conditions['validation_code']));
2008
2009
	$conditions['validation_code'] = substr(hash('sha256', $conditions['validation_code']), 0, 10);
2010
2011
	$available_conditions = array(
2012
		'time_before' => '
2013
				AND date_registered < {int:time_before}',
2014
		'members' => '
2015
				AND id_member IN ({array_int:members})',
2016
	);
2017
2018
	$query_cond = array();
2019
	foreach ($conditions as $key => $dummy)
2020
	{
2021
		if (isset($available_conditions[$key]))
2022
		{
2023
			$query_cond[] = $available_conditions[$key];
2024
		}
2025
	}
2026
2027
	$conditions['not_activated'] = 0;
2028
2029
	$db->query('', '
2030
		UPDATE {db_prefix}members
2031
		SET validation_code = {string:validation_code}, is_activated = {int:not_activated}
2032
		WHERE is_activated = {int:activated_status}
2033
			' . implode('', $query_cond) . '
2034
			AND id_member = {int:selected_member}',
2035
		$conditions
2036
	);
2037
}
2038
2039
/**
2040
 * Count members of a given group
2041
 *
2042
 * @package Members
2043
 * @param int $id_group
2044
 * @return int
2045
 */
2046
function countMembersInGroup($id_group = 0)
2047
{
2048
	$db = database();
2049
2050
	// Determine the number of ungrouped members.
2051
	$request = $db->query('', '
2052
		SELECT COUNT(*)
2053
		FROM {db_prefix}members
2054
		WHERE id_group = {int:group}',
2055
		array(
2056
			'group' => $id_group,
2057
		)
2058
	);
2059
	list ($num_members) = $db->fetch_row($request);
2060
	$db->free_result($request);
2061
2062
	return $num_members;
2063
}
2064
2065
/**
2066
 * Get the total amount of members online.
2067
 *
2068
 * @package Members
2069
 * @param string[] $conditions
2070
 * @return int
2071
 */
2072
function countMembersOnline($conditions)
2073
{
2074
	$db = database();
2075
2076
	$request = $db->query('', '
2077
		SELECT COUNT(*)
2078
		FROM {db_prefix}log_online AS lo
2079
			LEFT JOIN {db_prefix}members AS mem ON (lo.id_member = mem.id_member)' . (!empty($conditions) ? '
2080
		WHERE ' . implode(' AND ', $conditions) : ''),
2081
		array(
2082
		)
2083
	);
2084
	list ($totalMembers) = $db->fetch_row($request);
2085
	$db->free_result($request);
2086
2087
	return $totalMembers;
2088
}
2089
2090
/**
2091
 * Look for people online, provided they don't mind if you see they are.
2092
 *
2093
 * @package Members
2094
 * @param string[] $conditions
2095
 * @param string $sort_method
2096
 * @param string $sort_direction
2097
 * @param int $start
2098
 * @return array
2099
 */
2100
function onlineMembers($conditions, $sort_method, $sort_direction, $start)
2101
{
2102
	global $modSettings;
2103
2104
	$db = database();
2105
2106
	return $db->fetchQuery('
2107
		SELECT
2108
			lo.log_time, lo.id_member, lo.url, lo.ip, mem.real_name,
2109
			lo.session, mg.online_color, COALESCE(mem.show_online, 1) AS show_online,
2110
			lo.id_spider
2111
		FROM {db_prefix}log_online AS lo
2112
			LEFT JOIN {db_prefix}members AS mem ON (lo.id_member = mem.id_member)
2113
			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) ? '
2114
		WHERE ' . implode(' AND ', $conditions) : '') . '
2115
		ORDER BY {raw:sort_method} {raw:sort_direction}
2116
		LIMIT {int:offset}, {int:limit}',
2117
		array(
2118
			'regular_member' => 0,
2119
			'sort_method' => $sort_method,
2120
			'sort_direction' => $sort_direction == 'up' ? 'ASC' : 'DESC',
2121
			'offset' => $start,
2122
			'limit' => $modSettings['defaultMaxMembers'],
2123
		)
2124
	);
2125
}
2126
2127
/**
2128
 * Check if the OpenID URI is already registered for an existing member
2129
 *
2130
 * @package Members
2131
 * @param string $url
2132
 * @return array
2133
 */
2134
function memberExists($url)
2135
{
2136
	$db = database();
2137
2138
	$request = $db->query('', '
2139
		SELECT mem.id_member, mem.member_name
2140
		FROM {db_prefix}members AS mem
2141
		WHERE mem.openid_uri = {string:openid_uri}',
2142
		array(
2143
			'openid_uri' => $url,
2144
		)
2145
	);
2146
	$member = $db->fetch_assoc($request);
2147
	$db->free_result($request);
2148
2149
	return $member;
2150
}
2151
2152
/**
2153
 * Find the most recent members
2154
 *
2155
 * @package Members
2156
 * @param int $limit
2157
 */
2158
function recentMembers($limit)
2159
{
2160
	$db = database();
2161
2162
	// Find the most recent members.
2163
	return $db->fetchQuery('
2164
		SELECT id_member, member_name, real_name, date_registered, last_login
2165
		FROM {db_prefix}members
2166
		ORDER BY id_member DESC
2167
		LIMIT {int:limit}',
2168
		array(
2169
			'limit' => $limit,
2170
		)
2171
	);
2172
}
2173
2174
/**
2175
 * Assign membergroups to members.
2176
 *
2177
 * @package Members
2178
 * @param int $member
2179
 * @param int $primary_group
2180
 * @param int[] $additional_groups
2181
 */
2182
function assignGroupsToMember($member, $primary_group, $additional_groups)
2183
{
2184
	updateMemberData($member, array('id_group' => $primary_group, 'additional_groups' => implode(',', $additional_groups)));
2185
}
2186
2187
/**
2188
 * Get a list of members from a membergroups request.
2189
 *
2190
 * @package Members
2191
 * @param int[] $groups
2192
 * @param string $where
2193
 * @param boolean $change_groups = false
2194
 * @return mixed
2195
 */
2196
function getConcernedMembers($groups, $where, $change_groups = false)
2197
{
2198
	global $modSettings, $language;
2199
2200
	$db = database();
2201
2202
		// Get the details of all the members concerned...
2203
	$request = $db->query('', '
2204
		SELECT lgr.id_request, lgr.id_member, lgr.id_group, mem.email_address, mem.id_group AS primary_group,
2205
			mem.additional_groups AS additional_groups, mem.lngfile, mem.member_name, mem.notify_types,
2206
			mg.hidden, mg.group_name
2207
		FROM {db_prefix}log_group_requests AS lgr
2208
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = lgr.id_member)
2209
			INNER JOIN {db_prefix}membergroups AS mg ON (mg.id_group = lgr.id_group)
2210
		WHERE ' . $where . '
2211
			AND lgr.id_request IN ({array_int:request_list})
2212
		ORDER BY mem.lngfile',
2213
		array(
2214
			'request_list' => $groups,
2215
		)
2216
	);
2217
2218
	$email_details = array();
2219
	$group_changes = array();
2220
2221
	while ($row = $db->fetch_assoc($request))
2222
	{
2223
		$row['lngfile'] = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
2224
2225
		// If we are approving work out what their new group is.
2226
		if ($change_groups)
2227
		{
2228
			// For people with more than one request at once.
2229
			if (isset($group_changes[$row['id_member']]))
2230
			{
2231
				$row['additional_groups'] = $group_changes[$row['id_member']]['add'];
2232
				$row['primary_group'] = $group_changes[$row['id_member']]['primary'];
2233
			}
2234
			else
2235
				$row['additional_groups'] = explode(',', $row['additional_groups']);
2236
				// Don't have it already?
2237
			if ($row['primary_group'] == $row['id_group'] || in_array($row['id_group'], $row['additional_groups']))
2238
				continue;
2239
				// Should it become their primary?
2240
			if ($row['primary_group'] == 0 && $row['hidden'] == 0)
2241
				$row['primary_group'] = $row['id_group'];
2242
			else
2243
				$row['additional_groups'][] = $row['id_group'];
2244
2245
			// Add them to the group master list.
2246
			$group_changes[$row['id_member']] = array(
2247
				'primary' => $row['primary_group'],
2248
				'add' => $row['additional_groups'],
2249
			);
2250
		}
2251
2252
		// Add required information to email them.
2253
		if ($row['notify_types'] != 4)
2254
			$email_details[] = array(
2255
				'rid' => $row['id_request'],
2256
				'member_id' => $row['id_member'],
2257
				'member_name' => $row['member_name'],
2258
				'group_id' => $row['id_group'],
2259
				'group_name' => $row['group_name'],
2260
				'email' => $row['email_address'],
2261
				'language' => $row['lngfile'],
2262
			);
2263
	}
2264
	$db->free_result($request);
2265
2266
	$output = array(
2267
		'email_details' => $email_details,
2268
		'group_changes' => $group_changes
2269
	);
2270
2271
	return $output;
2272
}
2273
2274
/**
2275
 * Determine if the current user ($user_info) can contact another user ($who)
2276
 *
2277
 * @package Members
2278
 * @param int $who The id of the user to contact
2279
 */
2280
function canContact($who)
2281
{
2282
	global $user_info;
2283
2284
	$db = database();
2285
2286
	$request = $db->query('', '
2287
		SELECT receive_from, buddy_list, pm_ignore_list
2288
		FROM {db_prefix}members
2289
		WHERE id_member = {int:member}',
2290
		array(
2291
			'member' => $who,
2292
		)
2293
	);
2294
	list ($receive_from, $buddies, $ignore) = $db->fetch_row($request);
2295
	$db->free_result($request);
2296
2297
	$buddy_list = array_map('intval', explode(',', $buddies));
2298
	$ignore_list = array_map('intval', explode(',', $ignore));
2299
2300
	// 0 = all members
2301
	if ($receive_from == 0)
2302
		return true;
2303
	// 1 = all except ignore
2304
	elseif ($receive_from == 1)
2305
		return !(!empty($ignore_list) && in_array($user_info['id'], $ignore_list));
2306
	// 2 = buddies and admin
2307
	elseif ($receive_from == 2)
2308
		return ($user_info['is_admin'] || (!empty($buddy_list) && in_array($user_info['id'], $buddy_list)));
2309
	// 3 = admin only
2310
	else
2311
		return (bool) $user_info['is_admin'];
2312
}
2313
2314
/**
2315
 * This function updates the latest member, the total membercount, and the
2316
 * number of unapproved members.
2317
 *
2318
 * - It also only counts approved members when approval is on,
2319
 * but is much more efficient with it off.
2320
 *
2321
 * @package Members
2322
 * @param integer|null $id_member = null If not an integer reload from the database
2323
 * @param string|null $real_name = null
2324
 */
2325
function updateMemberStats($id_member = null, $real_name = null)
2326
{
2327
	global $modSettings;
2328
2329
	$db = database();
2330
2331
	$changes = array(
2332
		'memberlist_updated' => time(),
2333
	);
2334
2335
	// #1 latest member ID, #2 the real name for a new registration.
2336
	if (is_int($id_member))
2337
	{
2338
		$changes['latestMember'] = $id_member;
2339
		$changes['latestRealName'] = $real_name;
2340
2341
		updateSettings(array('totalMembers' => true), true);
2342
	}
2343
	// We need to calculate the totals.
2344
	else
2345
	{
2346
		// Update the latest activated member (highest id_member) and count.
2347
		$request = $db->query('', '
2348
			SELECT COUNT(*), MAX(id_member)
2349
			FROM {db_prefix}members
2350
			WHERE is_activated = {int:is_activated}',
2351
			array(
2352
				'is_activated' => 1,
2353
			)
2354
		);
2355
		list ($changes['totalMembers'], $changes['latestMember']) = $db->fetch_row($request);
2356
		$db->free_result($request);
2357
2358
		// Get the latest activated member's display name.
2359
		$request = getBasicMemberData((int) $changes['latestMember']);
2360
		$changes['latestRealName'] = $request['real_name'];
2361
2362
		// Are we using registration approval?
2363
		if ((!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 2) || !empty($modSettings['approveAccountDeletion']))
2364
		{
2365
			// Update the amount of members awaiting approval - ignoring COPPA accounts, as you can't approve them until you get permission.
2366
			$request = $db->query('', '
2367
				SELECT COUNT(*)
2368
				FROM {db_prefix}members
2369
				WHERE is_activated IN ({array_int:activation_status})',
2370
				array(
2371
					'activation_status' => array(3, 4),
2372
				)
2373
			);
2374
			list ($changes['unapprovedMembers']) = $db->fetch_row($request);
2375
			$db->free_result($request);
2376
		}
2377
	}
2378
2379
	updateSettings($changes);
2380
}
2381
2382
/**
2383
 * Builds the 'query_see_board' element for a certain member
2384
 *
2385
 * @package Members
2386
 * @param integer $id_member a valid member id
2387
 * @return string Query string
2388
 */
2389
function memberQuerySeeBoard($id_member)
2390
{
2391
	global $modSettings;
2392
2393
	$member = getBasicMemberData($id_member, array('moderation' => true));
2394
	if (empty($member))
2395
	{
2396
		return '0=1';
2397
	}
2398
2399
	if (empty($member['additional_groups']))
2400
	{
2401
		$groups = array($member['id_group'], $member['id_post_group']);
2402
	}
2403
	else
2404
	{
2405
		$groups = array_merge(
2406
			array($member['id_group'], $member['id_post_group']),
2407
			explode(',', $member['additional_groups'])
2408
		);
2409
	}
2410
2411
	foreach ($groups as $k => $v)
2412
	{
2413
		$groups[$k] = (int) $v;
2414
	}
2415
	$groups = array_unique($groups);
2416
2417
	if (in_array(1, $groups))
2418
	{
2419
		return '1=1';
2420
	}
2421
	else
2422
	{
2423
		require_once(SUBSDIR . '/Boards.subs.php');
2424
2425
		$boards_mod = boardsModerated($id_member);
2426
		$mod_query = empty($boards_mod) ? '' : ' OR b.id_board IN (' . implode(',', $boards_mod) . ')';
2427
2428
		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 . ')';
2429
	}
2430
}
2431
2432
/**
2433
 * Updates the columns in the members table.
2434
 *
2435
 * What it does:
2436
 *
2437
 * - Assumes the data has been htmlspecialchar'd, no sanitation is performed on the data.
2438
 * - This function should be used whenever member data needs to be updated in place of an UPDATE query.
2439
 * - $data is an associative array of the columns to be updated and their respective values.
2440
 * any string values updated should be quoted and slashed.
2441
 * - The value of any column can be '+' or '-', which mean 'increment' and decrement, respectively.
2442
 * - If the member's post number is updated, updates their post groups.
2443
 *
2444
 * @param int[]|int $members An array of member ids
2445
 * @param mixed[] $data An associative array of the columns to be updated and their respective values.
2446
 */
2447
function updateMemberData($members, $data)
2448
{
2449
	global $modSettings, $user_info;
2450
2451
	$db = database();
2452
2453
	$parameters = array();
2454
	if (is_array($members))
2455
	{
2456
		$condition = 'id_member IN ({array_int:members})';
2457
		$parameters['members'] = $members;
2458
	}
2459
	elseif ($members === null)
0 ignored issues
show
introduced by
The condition $members === null is always false.
Loading history...
2460
		$condition = '1=1';
2461
	else
2462
	{
2463
		$condition = 'id_member = {int:member}';
2464
		$parameters['member'] = $members;
2465
	}
2466
2467
	// Everything is assumed to be a string unless it's in the below.
2468
	$knownInts = array(
2469
		'date_registered', 'posts', 'id_group', 'last_login', 'personal_messages', 'unread_messages', 'mentions',
2470
		'new_pm', 'pm_prefs', 'hide_email', 'show_online', 'pm_email_notify', 'receive_from', 'karma_good', 'karma_bad',
2471
		'notify_announcements', 'notify_send_body', 'notify_regularity', 'notify_types',
2472
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning', 'likes_given',
2473
		'likes_received', 'enable_otp', 'otp_used'
2474
	);
2475
	$knownFloats = array(
2476
		'time_offset',
2477
	);
2478
2479
	if (!empty($modSettings['integrate_change_member_data']))
2480
	{
2481
		// Only a few member variables are really interesting for integration.
2482
		$integration_vars = array(
2483
			'member_name',
2484
			'real_name',
2485
			'email_address',
2486
			'id_group',
2487
			'birthdate',
2488
			'website_title',
2489
			'website_url',
2490
			'hide_email',
2491
			'time_format',
2492
			'time_offset',
2493
			'avatar',
2494
			'lngfile',
2495
		);
2496
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
2497
2498
		// Only proceed if there are any variables left to call the integration function.
2499
		if (count($vars_to_integrate) != 0)
2500
		{
2501
			// Fetch a list of member_names if necessary
2502
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
2503
				$member_names = array($user_info['username']);
2504
			else
2505
			{
2506
				$member_names = $db->fetchQueryCallback('
2507
					SELECT member_name
2508
					FROM {db_prefix}members
2509
					WHERE ' . $condition,
2510
					$parameters,
2511
					function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

2511
					/** @scrutinizer ignore-type */ function ($row)
Loading history...
2512
					{
2513
						return $row['member_name'];
2514
					}
2515
				);
2516
			}
2517
2518
			if (!empty($member_names))
2519
				foreach ($vars_to_integrate as $var)
2520
					call_integration_hook('integrate_change_member_data', array($member_names, &$var, &$data[$var], &$knownInts, &$knownFloats));
2521
		}
2522
	}
2523
2524
	$setString = '';
2525
	foreach ($data as $var => $val)
2526
	{
2527
		$type = 'string';
2528
2529
		if (in_array($var, $knownInts))
2530
			$type = 'int';
2531
		elseif (in_array($var, $knownFloats))
2532
			$type = 'float';
2533
		elseif ($var == 'birthdate')
2534
			$type = 'date';
2535
2536
		// Doing an increment?
2537
		if ($type == 'int' && ($val === '+' || $val === '-'))
2538
		{
2539
			$val = $var . ' ' . $val . ' 1';
2540
			$type = 'raw';
2541
		}
2542
2543
		// Ensure posts, personal_messages, and unread_messages don't overflow or underflow.
2544
		if (in_array($var, array('posts', 'personal_messages', 'unread_messages')))
2545
		{
2546
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
2547
			{
2548
				if ($match[1] != '+ ')
2549
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
2550
				$type = 'raw';
2551
			}
2552
		}
2553
2554
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
2555
		$parameters['p_' . $var] = $val;
2556
	}
2557
2558
	$db->query('', '
2559
		UPDATE {db_prefix}members
2560
		SET' . substr($setString, 0, -1) . '
2561
		WHERE ' . $condition,
2562
		$parameters
2563
	);
2564
2565
	require_once(SUBSDIR . '/Membergroups.subs.php');
2566
	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

2566
	updatePostGroupStats(/** @scrutinizer ignore-type */ $members, array_keys($data));
Loading history...
2567
2568
	$cache = Cache::instance();
2569
2570
	// Clear any caching?
2571
	if ($cache->levelHigherThan(1) && !empty($members))
2572
	{
2573
		if (!is_array($members))
2574
			$members = array($members);
2575
2576
		foreach ($members as $member)
2577
		{
2578
			if ($cache->levelHigherThan(2))
2579
			{
2580
				$cache->remove('member_data-profile-' . $member);
2581
				$cache->remove('member_data-normal-' . $member);
2582
				$cache->remove('member_data-minimal-' . $member);
2583
			}
2584
2585
			$cache->remove('user_settings-' . $member);
2586
		}
2587
	}
2588
}
2589
2590
/**
2591
 * Loads members who are associated with an ip address
2592
 *
2593
 * @param string $ip_string raw value to use in where clause
2594
 * @param string $ip_var
2595
 */
2596
function loadMembersIPs($ip_string, $ip_var)
2597
{
2598
	global $scripturl;
2599
2600
	$db = database();
2601
2602
	$request = $db->query('', '
2603
		SELECT
2604
			id_member, real_name AS display_name, member_ip
2605
		FROM {db_prefix}members
2606
		WHERE member_ip ' . $ip_string,
2607
		array(
2608
			'ip_address' => $ip_var,
2609
		)
2610
	);
2611
	$ips = array();
2612
	while ($row = $db->fetch_assoc($request))
2613
		$ips[$row['member_ip']][] = '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['display_name'] . '</a>';
2614
	$db->free_result($request);
2615
2616
	ksort($ips);
2617
2618
	return $ips;
2619
}
2620
2621
function registerAgreementAccepted($id_member, $ip, $agreement_version)
2622
{
2623
	$db = database();
2624
2625
	$db->insert('',
2626
		'{db_prefix}log_agreement_accept',
2627
		array(
2628
			'version' => 'date',
2629
			'id_member' => 'int',
2630
			'accepted_date' => 'date',
2631
			'accepted_ip' => 'string-255',
2632
		),
2633
		array(
2634
			array(
2635
				'version' => $agreement_version,
2636
				'id_member' => $id_member,
2637
				'accepted_date' => strftime('%Y-%m-%d', forum_time(false)),
2638
				'accepted_ip' => $ip,
2639
			)
2640
		),
2641
		array('version', 'id_member')
2642
	);
2643
}
2644
2645
/**
2646
 * Utility function to update a members salt to a new value
2647
 *
2648
 * @param int $id member to update
2649
 * @param bool $refresh if to always refresh to a new salt
2650
 * @param int $min if current salt lenght is less than this, gen a new one
2651
 * @return bool
2652
 */
2653
function updateMemberSalt($id, $refresh = false, $min = 9)
2654
{
2655
	global $user_settings;
2656
2657
	if (empty($user_settings['password_salt']))
2658
	{
2659
		return false;
2660
	}
2661
2662
	if ((strlen($user_settings['password_salt']) > $min) && !$refresh)
2663
	{
2664
		return false;
2665
	}
2666
2667
	$tokenizer = new Token_Hash();
2668
	$user_settings['password_salt'] = $tokenizer->generate_hash(16);
2669
	updateMemberData((int) $id, array('password_salt' => $user_settings['password_salt']));
2670
2671
	return true;
2672
}