forked from SimpleMachines/SMF
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Security.php
1521 lines (1311 loc) · 50.7 KB
/
Security.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* This file has the very important job of ensuring forum security.
* This task includes banning and permissions, namely.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Check if the user is who he/she says he is
* Makes sure the user is who they claim to be by requiring a password to be typed in every hour.
* Is turned on and off by the securityDisable setting.
* Uses the adminLogin() function of Subs-Auth.php if they need to login, which saves all request (post and get) data.
*
* @param string $type What type of session this is
* @param string $force When true, require a password even if we normally wouldn't
* @return void|string Returns 'session_verify_fail' if verification failed
*/
function validateSession($type = 'admin', $force = false)
{
global $modSettings, $sourcedir, $user_info;
// We don't care if the option is off, because Guests should NEVER get past here.
is_not_guest();
// Validate what type of session check this is.
$types = array();
call_integration_hook('integrate_validateSession', array(&$types));
$type = in_array($type, $types) || $type == 'moderate' ? $type : 'admin';
// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
$refreshTime = isset($_GET['xml']) ? 4200 : 3600;
if (empty($force))
{
// Is the security option off?
if (!empty($modSettings['securityDisable' . ($type != 'admin' ? '_' . $type : '')]))
return;
// Or are they already logged in?, Moderator or admin session is need for this area
if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
return;
}
require_once($sourcedir . '/Subs-Auth.php');
// Posting the password... check it.
if (isset($_POST[$type . '_pass']))
{
// Check to ensure we're forcing SSL for authentication
if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
fatal_lang_error('login_ssl_required');
checkSession();
$good_password = in_array(true, call_integration_hook('integrate_verify_password', array($user_info['username'], $_POST[$type . '_pass'], false)), true);
// Password correct?
if ($good_password || hash_verify_password($user_info['username'], $_POST[$type . '_pass'], $user_info['passwd']))
{
$_SESSION[$type . '_time'] = time();
unset($_SESSION['request_referer']);
return;
}
}
// Better be sure to remember the real referer
if (empty($_SESSION['request_referer']))
$_SESSION['request_referer'] = isset($_SERVER['HTTP_REFERER']) ? @parse_iri($_SERVER['HTTP_REFERER']) : array();
elseif (empty($_POST))
unset($_SESSION['request_referer']);
// Need to type in a password for that, man.
if (!isset($_GET['xml']))
adminLogin($type);
else
return 'session_verify_fail';
}
/**
* Require a user who is logged in. (not a guest.)
* Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
* Message is what to tell them when asking them to login.
*
* @param string $message The message to display to the guest
*/
function is_not_guest($message = '')
{
global $user_info, $txt, $context, $scripturl, $modSettings;
// Luckily, this person isn't a guest.
if (!$user_info['is_guest'])
return;
// Log what they were trying to do didn't work)
if (!empty($modSettings['who_enabled']))
$_GET['error'] = 'guest_login';
writeLog(true);
// Just die.
if (isset($_REQUEST['xml']))
obExit(false);
// Attempt to detect if they came from dlattach.
if (SMF != 'SSI' && empty($context['theme_loaded']))
loadTheme();
// Never redirect to an attachment
if (strpos($_SERVER['REQUEST_URL'], 'dlattach') === false)
$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
// Load the Login template and language file.
loadLanguage('Login');
// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
if (empty($context['template_layers']))
{
$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
redirectexit('action=login');
}
else
{
loadTemplate('Login');
$context['sub_template'] = 'kick_guest';
$context['robot_no_index'] = true;
}
// Use the kick_guest sub template...
$context['kick_message'] = $message;
$context['page_title'] = $txt['login'];
obExit();
// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
trigger_error('No direct access...', E_USER_ERROR);
}
/**
* Do banning related stuff. (ie. disallow access....)
* Checks if the user is banned, and if so dies with an error.
* Caches this information for optimization purposes.
*
* @param bool $forceCheck Whether to force a recheck
*/
function is_not_banned($forceCheck = false)
{
global $txt, $modSettings, $context, $user_info;
global $sourcedir, $cookiename, $user_settings, $smcFunc;
// You cannot be banned if you are an admin - doesn't help if you log out.
if ($user_info['is_admin'])
return;
// Only check the ban every so often. (to reduce load.)
if ($forceCheck || !isset($_SESSION['ban']) || empty($modSettings['banLastUpdated']) || ($_SESSION['ban']['last_checked'] < $modSettings['banLastUpdated']) || $_SESSION['ban']['id_member'] != $user_info['id'] || $_SESSION['ban']['ip'] != $user_info['ip'] || $_SESSION['ban']['ip2'] != $user_info['ip2'] || (isset($user_info['email'], $_SESSION['ban']['email']) && $_SESSION['ban']['email'] != $user_info['email']))
{
// Innocent until proven guilty. (but we know you are! :P)
$_SESSION['ban'] = array(
'last_checked' => time(),
'id_member' => $user_info['id'],
'ip' => $user_info['ip'],
'ip2' => $user_info['ip2'],
'email' => $user_info['email'],
);
$ban_query = array();
$ban_query_vars = array('current_time' => time());
$flag_is_activated = false;
// Check both IP addresses.
foreach (array('ip', 'ip2') as $ip_number)
{
if ($ip_number == 'ip2' && $user_info['ip2'] == $user_info['ip'])
continue;
$ban_query[] = ' {inet:' . $ip_number . '} BETWEEN bi.ip_low and bi.ip_high';
$ban_query_vars[$ip_number] = $user_info[$ip_number];
// IP was valid, maybe there's also a hostname...
if (empty($modSettings['disableHostnameLookup']) && $user_info[$ip_number] != 'unknown')
{
$hostname = host_from_ip($user_info[$ip_number]);
if (strlen($hostname) > 0)
{
$ban_query[] = '({string:hostname' . $ip_number . '} LIKE bi.hostname)';
$ban_query_vars['hostname' . $ip_number] = $hostname;
}
}
}
// Is their email address banned?
if (strlen($user_info['email']) != 0)
{
$ban_query[] = '({string:email} LIKE bi.email_address)';
$ban_query_vars['email'] = $user_info['email'];
}
// How about this user?
if (!$user_info['is_guest'] && !empty($user_info['id']))
{
$ban_query[] = 'bi.id_member = {int:id_member}';
$ban_query_vars['id_member'] = $user_info['id'];
}
// Check the ban, if there's information.
if (!empty($ban_query))
{
$restrictions = array(
'cannot_access',
'cannot_login',
'cannot_post',
'cannot_register',
);
$request = $smcFunc['db_query']('', '
SELECT bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
FROM {db_prefix}ban_items AS bi
INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time}))
WHERE
(' . implode(' OR ', $ban_query) . ')',
$ban_query_vars
);
// Store every type of ban that applies to you in your session.
while ($row = $smcFunc['db_fetch_assoc']($request))
{
foreach ($restrictions as $restriction)
if (!empty($row[$restriction]))
{
$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
$_SESSION['ban']['expire_time'] = $row['expire_time'];
if (!$user_info['is_guest'] && $restriction == 'cannot_access' && ($row['id_member'] == $user_info['id'] || $row['email_address'] == $user_info['email']))
$flag_is_activated = true;
}
}
$smcFunc['db_free_result']($request);
}
// Mark the cannot_access and cannot_post bans as being 'hit'.
if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
log_ban(array_merge(isset($_SESSION['ban']['cannot_access']) ? $_SESSION['ban']['cannot_access']['ids'] : array(), isset($_SESSION['ban']['cannot_post']) ? $_SESSION['ban']['cannot_post']['ids'] : array(), isset($_SESSION['ban']['cannot_login']) ? $_SESSION['ban']['cannot_login']['ids'] : array()));
// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
if ($user_info['id'] && (($user_settings['is_activated'] >= 10 && !$flag_is_activated)
|| ($user_settings['is_activated'] < 10 && $flag_is_activated)))
{
require_once($sourcedir . '/ManageBans.php');
updateBanMembers();
}
}
// Hey, I know you! You're ehm...
if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
{
$bans = explode(',', $_COOKIE[$cookiename . '_']);
foreach ($bans as $key => $value)
$bans[$key] = (int) $value;
$request = $smcFunc['db_query']('', '
SELECT bi.id_ban, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
FROM {db_prefix}ban_items AS bi
INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
WHERE bi.id_ban IN ({array_int:ban_list})
AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
AND bg.cannot_access = {int:cannot_access}
LIMIT {int:limit}',
array(
'cannot_access' => 1,
'ban_list' => $bans,
'current_time' => time(),
'limit' => count($bans),
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
$_SESSION['ban']['expire_time'] = $row['expire_time'];
}
$smcFunc['db_free_result']($request);
// My mistake. Next time better.
if (!isset($_SESSION['ban']['cannot_access']))
{
require_once($sourcedir . '/Subs-Auth.php');
$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
smf_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
}
}
// If you're fully banned, it's end of the story for you.
if (isset($_SESSION['ban']['cannot_access']))
{
// We don't wanna see you!
if (!$user_info['is_guest'])
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_online
WHERE id_member = {int:current_member}',
array(
'current_member' => $user_info['id'],
)
);
// 'Log' the user out. Can't have any funny business... (save the name!)
$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
$user_info['name'] = '';
$user_info['username'] = '';
$user_info['is_guest'] = true;
$user_info['is_admin'] = false;
$user_info['permissions'] = array();
$user_info['id'] = 0;
$context['user'] = array(
'id' => 0,
'username' => '',
'name' => $txt['guest_title'],
'is_guest' => true,
'is_logged' => false,
'is_admin' => false,
'is_mod' => false,
'can_mod' => false,
'language' => $user_info['language'],
);
// A goodbye present.
require_once($sourcedir . '/Subs-Auth.php');
require_once($sourcedir . '/LogInOut.php');
$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
smf_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
// Don't scare anyone, now.
$_GET['action'] = '';
$_GET['board'] = '';
$_GET['topic'] = '';
writeLog(true);
Logout(true, false);
// You banned, sucka!
fatal_error(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_access']['reason']) ? '' : '<br>' . $_SESSION['ban']['cannot_access']['reason']) . '<br>' . (!empty($_SESSION['ban']['expire_time']) ? sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)) : $txt['your_ban_expires_never']), false, 403);
// If we get here, something's gone wrong.... but let's try anyway.
trigger_error('No direct access...', E_USER_ERROR);
}
// You're not allowed to log in but yet you are. Let's fix that.
elseif (isset($_SESSION['ban']['cannot_login']) && !$user_info['is_guest'])
{
// We don't wanna see you!
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_online
WHERE id_member = {int:current_member}',
array(
'current_member' => $user_info['id'],
)
);
// 'Log' the user out. Can't have any funny business... (save the name!)
$old_name = isset($user_info['name']) && $user_info['name'] != '' ? $user_info['name'] : $txt['guest_title'];
$user_info['name'] = '';
$user_info['username'] = '';
$user_info['is_guest'] = true;
$user_info['is_admin'] = false;
$user_info['permissions'] = array();
$user_info['id'] = 0;
$context['user'] = array(
'id' => 0,
'username' => '',
'name' => $txt['guest_title'],
'is_guest' => true,
'is_logged' => false,
'is_admin' => false,
'is_mod' => false,
'can_mod' => false,
'language' => $user_info['language'],
);
// SMF's Wipe 'n Clean(r) erases all traces.
$_GET['action'] = '';
$_GET['board'] = '';
$_GET['topic'] = '';
writeLog(true);
require_once($sourcedir . '/LogInOut.php');
Logout(true, false);
fatal_error(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_login']['reason']) ? '' : '<br>' . $_SESSION['ban']['cannot_login']['reason']) . '<br>' . (!empty($_SESSION['ban']['expire_time']) ? sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)) : $txt['your_ban_expires_never']) . '<br>' . $txt['ban_continue_browse'], false, 403);
}
// Fix up the banning permissions.
if (isset($user_info['permissions']))
banPermissions();
}
/**
* Fix permissions according to ban status.
* Applies any states of banning by removing permissions the user cannot have.
*/
function banPermissions()
{
global $user_info, $sourcedir, $modSettings, $context;
// Somehow they got here, at least take away all permissions...
if (isset($_SESSION['ban']['cannot_access']))
$user_info['permissions'] = array();
// Okay, well, you can watch, but don't touch a thing.
elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $user_info['warning']))
{
$denied_permissions = array(
'pm_send',
'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
'poll_post',
'poll_add_own', 'poll_add_any',
'poll_edit_own', 'poll_edit_any',
'poll_lock_own', 'poll_lock_any',
'poll_remove_own', 'poll_remove_any',
'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
'profile_identity_any', 'profile_extra_any', 'profile_title_any',
'profile_forum_any', 'profile_other_any', 'profile_signature_any',
'post_new', 'post_reply_own', 'post_reply_any',
'delete_own', 'delete_any', 'delete_replies',
'make_sticky',
'merge_any', 'split_any',
'modify_own', 'modify_any', 'modify_replies',
'move_any',
'lock_own', 'lock_any',
'remove_own', 'remove_any',
'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
);
call_integration_hook('integrate_post_ban_permissions', array(&$denied_permissions));
$user_info['permissions'] = array_diff($user_info['permissions'], $denied_permissions);
}
// Are they absolutely under moderation?
elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $user_info['warning'])
{
// Work out what permissions should change...
$permission_change = array(
'post_new' => 'post_unapproved_topics',
'post_reply_own' => 'post_unapproved_replies_own',
'post_reply_any' => 'post_unapproved_replies_any',
'post_attachment' => 'post_unapproved_attachments',
);
call_integration_hook('integrate_warn_permissions', array(&$permission_change));
foreach ($permission_change as $old => $new)
{
if (!in_array($old, $user_info['permissions']))
unset($permission_change[$old]);
else
$user_info['permissions'][] = $new;
}
$user_info['permissions'] = array_diff($user_info['permissions'], array_keys($permission_change));
}
// @todo Find a better place to call this? Needs to be after permissions loaded!
// Finally, some bits we cache in the session because it saves queries.
if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == $user_info['id'])
$user_info['mod_cache'] = $_SESSION['mc'];
else
{
require_once($sourcedir . '/Subs-Auth.php');
rebuildModCache();
}
// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
if (isset($_SESSION['rc']['reports']) && isset($_SESSION['rc']['member_reports']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == $user_info['id'])
{
$context['open_mod_reports'] = $_SESSION['rc']['reports'];
$context['open_member_reports'] = $_SESSION['rc']['member_reports'];
}
elseif ($_SESSION['mc']['bq'] != '0=1')
{
require_once($sourcedir . '/Subs-ReportedContent.php');
$context['open_mod_reports'] = recountOpenReports('posts');
$context['open_member_reports'] = recountOpenReports('members');
}
else
{
$context['open_mod_reports'] = 0;
$context['open_member_reports'] = 0;
}
}
/**
* Log a ban in the database.
* Log the current user in the ban logs.
* Increment the hit counters for the specified ban ID's (if any.)
*
* @param array $ban_ids The IDs of the bans
* @param string $email The email address associated with the user that triggered this hit
*/
function log_ban($ban_ids = array(), $email = null)
{
global $user_info, $smcFunc;
// Don't log web accelerators, it's very confusing...
if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
return;
$smcFunc['db_insert']('',
'{db_prefix}log_banned',
array('id_member' => 'int', 'ip' => 'inet', 'email' => 'string', 'log_time' => 'int'),
array($user_info['id'], $user_info['ip'], ($email === null ? ($user_info['is_guest'] ? '' : $user_info['email']) : $email), time()),
array('id_ban_log')
);
// One extra point for these bans.
if (!empty($ban_ids))
$smcFunc['db_query']('', '
UPDATE {db_prefix}ban_items
SET hits = hits + 1
WHERE id_ban IN ({array_int:ban_ids})',
array(
'ban_ids' => $ban_ids,
)
);
}
/**
* Checks if a given email address might be banned.
* Check if a given email is banned.
* Performs an immediate ban if the turns turns out positive.
*
* @param string $email The email to check
* @param string $restriction What type of restriction (cannot_post, cannot_register, etc.)
* @param string $error The error message to display if they are indeed banned
*/
function isBannedEmail($email, $restriction, $error)
{
global $txt, $smcFunc;
// Can't ban an empty email
if (empty($email) || trim($email) == '')
return;
// Let's start with the bans based on your IP/hostname/memberID...
$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : array();
$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
// ...and add to that the email address you're trying to register.
$request = $smcFunc['db_query']('', '
SELECT bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
FROM {db_prefix}ban_items AS bi
INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
WHERE {string:email} LIKE bi.email_address
AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
array(
'email' => $email,
'cannot_access' => 1,
'now' => time(),
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
if (!empty($row['cannot_access']))
{
$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
}
if (!empty($row[$restriction]))
{
$ban_ids[] = $row['id_ban'];
$ban_reason = $row['reason'];
}
}
$smcFunc['db_free_result']($request);
// You're in biiig trouble. Banned for the rest of this session!
if (isset($_SESSION['ban']['cannot_access']))
{
log_ban($_SESSION['ban']['cannot_access']['ids']);
$_SESSION['ban']['last_checked'] = time();
fatal_error(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
}
if (!empty($ban_ids))
{
// Log this ban for future reference.
log_ban($ban_ids, $email);
fatal_error($error . $ban_reason, false);
}
}
/**
* Make sure the user's correct session was passed, and they came from here.
* Checks the current session, verifying that the person is who he or she should be.
* Also checks the referrer to make sure they didn't get sent here.
* Depends on the disableCheckUA setting, which is usually missing.
* Will check GET, POST, or REQUEST depending on the passed type.
* Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
*
* @param string $type The type of check (post, get, request)
* @param string $from_action The action this is coming from
* @param bool $is_fatal Whether to die with a fatal error if the check fails
* @return string The error message if is_fatal is false.
*/
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
{
global $context, $sc, $modSettings, $boardurl;
// Is it in as $_POST['sc']?
if ($type == 'post')
{
$check = isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
if ($check !== $sc)
$error = 'session_timeout';
}
// How about $_GET['sesc']?
elseif ($type == 'get')
{
$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
if ($check !== $sc)
$error = 'session_verify_fail';
}
// Or can it be in either?
elseif ($type == 'request')
{
$check = isset($_GET[$_SESSION['session_var']]) ? $_GET[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : (isset($_POST[$_SESSION['session_var']]) ? $_POST[$_SESSION['session_var']] : (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null)));
if ($check !== $sc)
$error = 'session_verify_fail';
}
// Verify that they aren't changing user agents on us - that could be bad.
if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] != $_SERVER['HTTP_USER_AGENT']) && empty($modSettings['disableCheckUA']))
$error = 'session_verify_fail';
// Make sure a page with session check requirement is not being prefetched.
if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
{
ob_end_clean();
send_http_status(403);
die;
}
// Check the referring site - it should be the same server at least!
if (isset($_SESSION['request_referer']))
$referrer = $_SESSION['request_referer'];
else
$referrer = isset($_SERVER['HTTP_REFERER']) ? @parse_url($_SERVER['HTTP_REFERER']) : array();
// Check the refer but if we have CORS enabled and it came from a trusted source, we can skip this check.
if (!empty($referrer['host']) && (empty($modSettings['allow_cors']) || empty($context['valid_cors_found']) || !in_array($context['valid_cors_found'], array('same', 'subdomain'))))
{
if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
else
$real_host = $_SERVER['HTTP_HOST'];
$parsed_url = parse_iri($boardurl);
// Are global cookies on? If so, let's check them ;).
if (!empty($modSettings['globalCookies']))
{
if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
$parsed_url['host'] = $parts[1];
if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
$referrer['host'] = $parts[1];
if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
$real_host = $parts[1];
}
// Okay: referrer must either match parsed_url or real_host.
if (isset($parsed_url['host']) && strtolower($referrer['host']) != strtolower($parsed_url['host']) && strtolower($referrer['host']) != strtolower($real_host))
{
$error = 'verify_url_fail';
$log_error = true;
}
}
// Well, first of all, if a from_action is specified you'd better have an old_url.
if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) == 0))
{
$error = 'verify_url_fail';
$log_error = true;
}
if (strtolower($_SERVER['HTTP_USER_AGENT']) == 'hacker')
fatal_error('Sound the alarm! It\'s a hacker! Close the castle gates!!', false);
// Everything is ok, return an empty string.
if (!isset($error))
return '';
// A session error occurred, show the error.
elseif ($is_fatal)
{
if (isset($_GET['xml']))
{
ob_end_clean();
send_http_status(403, 'Forbidden - Session timeout');
die;
}
else
fatal_lang_error($error, isset($log_error) ? 'user' : false);
}
// A session error occurred, return the error to the calling function.
else
return $error;
// We really should never fall through here, for very important reasons. Let's make sure.
trigger_error('No direct access...', E_USER_ERROR);
}
/**
* Check if a specific confirm parameter was given.
*
* @param string $action The action we want to check against
* @return bool|string True if the check passed or a token
*/
function checkConfirm($action)
{
global $modSettings, $smcFunc;
if (isset($_GET['confirm']) && isset($_SESSION['confirm_' . $action]) && md5($_GET['confirm'] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['confirm_' . $action])
return true;
else
{
$token = md5($smcFunc['random_int']() . session_id() . (string) microtime() . $modSettings['rand_seed']);
$_SESSION['confirm_' . $action] = md5($token . $_SERVER['HTTP_USER_AGENT']);
return $token;
}
}
/**
* Lets give you a token of our appreciation.
*
* @param string $action The action to create the token for
* @param string $type The type of token ('post', 'get' or 'request')
* @return array An array containing the name of the token var and the actual token
*/
function createToken($action, $type = 'post')
{
global $modSettings, $context, $smcFunc;
$token = md5($smcFunc['random_int']() . session_id() . (string) microtime() . $modSettings['rand_seed'] . $type);
$token_var = substr(preg_replace('~^\d+~', '', md5($smcFunc['random_int']() . (string) microtime() . $smcFunc['random_int']())), 0, $smcFunc['random_int'](7, 12));
$_SESSION['token'][$type . '-' . $action] = array($token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token);
$context[$action . '_token'] = $token;
$context[$action . '_token_var'] = $token_var;
return array($action . '_token_var' => $token_var, $action . '_token' => $token);
}
/**
* Only patrons with valid tokens can ride this ride.
*
* @param string $action The action to validate the token for
* @param string $type The type of request (get, request, or post)
* @param bool $reset Whether to reset the token and display an error if validation fails
* @return bool returns whether the validation was successful
*/
function validateToken($action, $type = 'post', $reset = true)
{
$type = $type == 'get' || $type == 'request' ? $type : 'post';
// This nasty piece of code validates a token.
/*
1. The token exists in session.
2. The {$type} variable should exist.
3. We concat the variable we received with the user agent
4. Match that result against what is in the session.
5. If it matches, success, otherwise we fallout.
*/
if (isset($_SESSION['token'][$type . '-' . $action], $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]]) && md5($GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]] . $_SERVER['HTTP_USER_AGENT']) === $_SESSION['token'][$type . '-' . $action][1])
{
// Invalidate this token now.
unset($_SESSION['token'][$type . '-' . $action]);
return true;
}
// Patrons with invalid tokens get the boot.
if ($reset)
{
// Might as well do some cleanup on this.
cleanTokens();
// I'm back baby.
createToken($action, $type);
fatal_lang_error('token_verify_fail', false);
}
// Remove this token as its useless
else
unset($_SESSION['token'][$type . '-' . $action]);
// Randomly check if we should remove some older tokens.
if (mt_rand(0, 138) == 23)
cleanTokens();
return false;
}
/**
* Removes old unused tokens from session
* defaults to 3 hours before a token is considered expired
* if $complete = true will remove all tokens
*
* @param bool $complete Whether to remove all tokens or only expired ones
*/
function cleanTokens($complete = false)
{
// We appreciate cleaning up after yourselves.
if (!isset($_SESSION['token']))
return;
// Clean up tokens, trying to give enough time still.
foreach ($_SESSION['token'] as $key => $data)
if ($data[2] + 10800 < time() || $complete)
unset($_SESSION['token'][$key]);
}
/**
* Check whether a form has been submitted twice.
* Registers a sequence number for a form.
* Checks whether a submitted sequence number is registered in the current session.
* Depending on the value of is_fatal shows an error or returns true or false.
* Frees a sequence number from the stack after it's been checked.
* Frees a sequence number without checking if action == 'free'.
*
* @param string $action The action - can be 'register', 'check' or 'free'
* @param bool $is_fatal Whether to die with a fatal error
* @return void|bool If the action isn't check, returns nothing, otherwise returns whether the check was successful
*/
function checkSubmitOnce($action, $is_fatal = true)
{
global $context, $txt;
if (!isset($_SESSION['forms']))
$_SESSION['forms'] = array();
// Register a form number and store it in the session stack. (use this on the page that has the form.)
if ($action == 'register')
{
$context['form_sequence_number'] = 0;
while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms']))
$context['form_sequence_number'] = mt_rand(1, 16000000);
}
// Check whether the submitted number can be found in the session.
elseif ($action == 'check')
{
if (!isset($_REQUEST['seqnum']))
return true;
elseif (!in_array($_REQUEST['seqnum'], $_SESSION['forms']))
{
$_SESSION['forms'][] = (int) $_REQUEST['seqnum'];
return true;
}
elseif ($is_fatal)
fatal_lang_error('error_form_already_submitted', false);
else
return false;
}
// Don't check, just free the stack number.
elseif ($action == 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms']))
$_SESSION['forms'] = array_diff($_SESSION['forms'], array($_REQUEST['seqnum']));
elseif ($action != 'free')
{
loadLanguage('Errors');
trigger_error(sprintf($txt['check_submit_once_invalid_action'], $action), E_USER_WARNING);
}
}
/**
* Check the user's permissions.
* checks whether the user is allowed to do permission. (ie. post_new.)
* If boards is specified, checks those boards instead of the current one.
* If any is true, will return true if the user has the permission on any of the specified boards
* Always returns true if the user is an administrator.
*
* @param string|array $permission A single permission to check or an array of permissions to check
* @param int|array $boards The ID of a board or an array of board IDs if we want to check board-level permissions
* @param bool $any Whether to check for permission on at least one board instead of all boards
* @return bool Whether the user has the specified permission
*/
function allowedTo($permission, $boards = null, $any = false)
{
global $user_info, $smcFunc;
static $perm_cache = array();
// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
if (empty($permission))
return true;
// You're never allowed to do something if your data hasn't been loaded yet!
if (empty($user_info) || !isset($user_info['permissions']))
return false;
// Administrators are supermen :P.
if ($user_info['is_admin'])
return true;
// Let's ensure this is an array.
$permission = (array) $permission;
// This should be a boolean.
$any = (bool) $any;
// Are we checking the _current_ board, or some other boards?
if ($boards === null)
{
$user_permissions = $user_info['permissions'];
// Allow temporary overrides for general permissions?
call_integration_hook('integrate_allowed_to_general', array(&$user_permissions, $permission));
if (count(array_intersect($permission, $user_permissions)) != 0)
return true;
// You aren't allowed, by default.
else
return false;
}
elseif (!is_array($boards))
$boards = array($boards);
$cache_key = hash('md5', $user_info['id'] . '-' . implode(',', $permission) . '-' . implode(',', $boards) . '-' . (int) $any);
if (isset($perm_cache[$cache_key]))
return $perm_cache[$cache_key];
$request = $smcFunc['db_query']('', '
SELECT MIN(bp.add_deny) AS add_deny
FROM {db_prefix}boards AS b
INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
WHERE b.id_board IN ({array_int:board_list})
AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
AND bp.permission IN ({array_string:permission_list})
AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})
GROUP BY b.id_board',
array(
'current_member' => $user_info['id'],
'board_list' => $boards,
'group_list' => $user_info['groups'],
'moderator_group' => 3,
'permission_list' => $permission,
)
);
if ($any)
{
$result = false;
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$result = !empty($row['add_deny']);
if ($result == true)
break;
}
$smcFunc['db_free_result']($request);
$return = $result;
}
// Make sure they can do it on all of the boards.
elseif ($smcFunc['db_num_rows']($request) != count($boards))
$return = false;
else
{
$result = true;
while ($row = $smcFunc['db_fetch_assoc']($request))
$result &= !empty($row['add_deny']);
$smcFunc['db_free_result']($request);
$return = $result;
}
// Allow temporary overrides for board permissions?
call_integration_hook('integrate_allowed_to_board', array(&$return, $permission, $boards, $any));
$perm_cache[$cache_key] = $return;
// If the query returned 1, they can do it... otherwise, they can't.
return $return;
}
/**
* Fatal error if they cannot.
* Uses allowedTo() to check if the user is allowed to do permission.
* Checks the passed boards or current board for the permission.
* If $any is true, the user only needs permission on at least one of the boards to pass
* If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
* If they are a guest and cannot do it, this calls is_not_guest().
*