Limit Login Attempts Reloaded - Version 2.3.0

Version Description

  • IP addresses can be white-listed now. https://wordpress.org/support/topic/legal-user/
  • A "Gateway" column is added to the lockouts log. It shows what endpoint an attacker was blocked from. https://wordpress.org/support/topic/xmlrpc-7/
  • The "Undefined index: client_type" error is fixed. https://wordpress.org/support/topic/php-notice-when-updating-settings-page/
Download this release

Release Info

Developer wpchefgadget
Plugin Icon 128x128 Limit Login Attempts Reloaded
Version 2.3.0
Comparing to
See all releases

Code changes from version 2.2.0 to 2.3.0

core/LimitLoginAttempts.php CHANGED
@@ -5,134 +5,142 @@
5
  */
6
  class Limit_Login_Attempts {
7
 
8
- /**
9
- * Main plugin options
10
- * @var array
11
- */
12
- public $_options = array(
13
- /* Are we behind a proxy? */
14
- 'client_type' => LLA_DIRECT_ADDR,
15
-
16
- /* Lock out after this many tries */
17
- 'allowed_retries' => 4,
18
-
19
- /* Lock out for this many seconds */
20
- 'lockout_duration' => 1200, // 20 minutes
21
-
22
- /* Long lock out after this many lockouts */
23
- 'allowed_lockouts' => 4,
24
-
25
- /* Long lock out for this many seconds */
26
- 'long_duration' => 86400, // 24 hours,
27
-
28
- /* Reset failed attempts after this many seconds */
29
- 'valid_duration' => 43200, // 12 hours
30
-
31
- /* Also limit malformed/forged cookies? */
32
- 'cookies' => true,
33
-
34
- /* Notify on lockout. Values: '', 'log', 'email', 'log,email' */
35
- 'lockout_notify' => 'log',
36
-
37
- /* If notify by email, do so after this number of lockouts */
38
- 'notify_email_after' => 4,
39
- );
40
-
41
- /**
42
- * Admin options page slug
43
- * @var string
44
- */
45
- private $_options_page_slug = 'limit-login-attempts';
46
-
47
- /**
48
- * Errors messages
49
- *
50
- * @var array
51
- */
52
- public $_errors = array();
53
-
54
- public function __construct() {
55
- $this->hooks_init();
56
- }
57
-
58
- /**
59
- * Register wp hooks and filters
60
- */
61
- public function hooks_init() {
62
- add_action( 'plugins_loaded', array($this, 'setup'), 9999 );
63
- add_action( 'admin_enqueue_scripts', array($this, 'enqueue') );
64
- }
65
-
66
- /**
67
- * @param $error IXR_Error
68
- *
69
- * @return IXR_Error
70
- */
71
- public function xmlrpc_error_messages( $error ) {
72
-
73
- if( !class_exists( 'IXR_Error' ) ) {
74
- return $error;
75
- }
76
-
77
- if( !$this->is_limit_login_ok() ) {
78
- return new IXR_Error( 403, $this->error_msg() );
79
- }
80
-
81
- $ip = $this->get_address();
82
- $retries = get_option( 'limit_login_retries' );
83
- $valid = get_option( 'limit_login_retries_valid' );
84
-
85
- /* Should we show retries remaining? */
86
-
87
- if( !is_array( $retries ) || !is_array( $valid ) ) {
88
- /* no retries at all */
89
- return $error;
90
- }
91
- if( !isset( $retries[ $ip ] ) || !isset( $valid[ $ip ] ) || time() > $valid[ $ip ] ) {
92
- /* no: no valid retries */
93
- return $error;
94
- }
95
- if( ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) == 0 ) {
96
- /* no: already been locked out for these retries */
97
- return $error;
98
- }
99
-
100
- $remaining = max( ( $this->get_option( 'allowed_retries' ) - ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) ), 0 );
101
- return new IXR_Error( 403, sprintf( _n( "<strong>%d</strong> attempt remaining.", "<strong>%d</strong> attempts remaining.", $remaining, 'limit-login-attempts-reloaded' ), $remaining ) );
102
- }
103
-
104
- /**
105
- * Errors on WooCommerce account page
106
- */
107
- public function add_wc_notices() {
108
-
109
- global $limit_login_just_lockedout, $limit_login_nonempty_credentials, $limit_login_my_error_shown;
110
-
111
- if( !function_exists( 'is_account_page' ) || !function_exists( 'wc_add_notice' ) ) {
112
- return;
113
- }
114
-
115
- /*
116
- * During lockout we do not want to show any other error messages (like
117
- * unknown user or empty password).
118
- */
119
- if( empty( $_POST ) && !$this->is_limit_login_ok() && !$limit_login_just_lockedout ) {
120
- if( is_account_page() ) {
121
- wc_add_notice( $this->error_msg(), 'error' );
122
- }
123
- }
124
-
125
- }
126
-
127
- public function setup() {
128
- $this->check_original_installed();
129
-
130
- load_plugin_textdomain( 'limit-login-attempts-reloaded', false, plugin_basename( dirname( __FILE__ ) ) . '/../languages' );
131
- $this->setup_options();
132
-
133
- add_action( 'wp_login_failed', array($this, 'limit_login_failed') );
134
-
135
- // TODO: remove this
 
 
 
 
 
 
 
 
136
  // if( $this->get_option( 'cookies' ) ) {
137
  // $this->handle_cookies();
138
  //
@@ -148,903 +156,935 @@ class Limit_Login_Attempts {
148
  // }
149
  // }
150
 
151
- add_filter( 'wp_authenticate_user', array($this, 'wp_authenticate_user'), 99999, 2 );
152
- add_filter( 'shake_error_codes', array($this, 'failure_shake') );
153
- add_action( 'login_head', array($this, 'add_error_message') );
154
- add_action( 'login_errors', array($this, 'fixup_error_messages') );
155
- add_action( 'admin_menu', array($this, 'admin_menu') );
156
-
157
- // Add notices for XMLRPC request
158
- add_filter( 'xmlrpc_login_error', array($this, 'xmlrpc_error_messages') );
159
-
160
- // Add notices to woocommerce login page
161
- add_action( 'wp_head', array($this, 'add_wc_notices') );
162
-
163
- /*
164
- * This action should really be changed to the 'authenticate' filter as
165
- * it will probably be deprecated. That is however only available in
166
- * later versions of WP.
167
- */
168
- add_action( 'wp_authenticate', array($this, 'track_credentials'), 10, 2 );
169
- }
170
-
171
- /**
172
- * Check if the original plugin is installed
173
- */
174
- private function check_original_installed() {
175
-
176
- if( defined( 'LIMIT_LOGIN_DIRECT_ADDR' ) ) { // Original plugin is installed
177
-
178
- if ( $active_plugins = get_option('active_plugins') ) {
179
- $deactivate_this = array(
180
- 'limit-login-attempts-reloaded/limit-login-attempts-reloaded.php'
181
- );
182
- $active_plugins = array_diff( $active_plugins, $deactivate_this );
183
- update_option( 'active_plugins', $active_plugins );
184
-
185
- add_action( 'admin_notices', function(){
186
- ?>
187
- <div class="notice notice-error is-dismissible">
188
- <p><?php _e( 'Please deactivate the Limit Login Attempts first.', 'limit-login-attempts-reloaded' ); ?></p>
189
- </div>
190
- <?php
191
- } );
192
- }
193
- }
194
- }
195
-
196
- /**
197
- * Enqueue js and css
198
- */
199
- public function enqueue() {
200
- wp_enqueue_style( 'lla-main', LLA_PLUGIN_URL . '/assets/css/limit-login-attempts.css' );
201
- }
202
-
203
- /**
204
- * Add admin options page
205
- */
206
- public function admin_menu() {
207
- global $wp_version;
208
-
209
- // Modern WP?
210
- if( version_compare( $wp_version, '3.0', '>=' ) ) {
211
- add_options_page( 'Limit Login Attempts', 'Limit Login Attempts', 'manage_options', $this->_options_page_slug, array($this, 'options_page') );
212
-
213
- return;
214
- }
215
-
216
- // Older WPMU?
217
- if( function_exists( "get_current_site" ) ) {
218
- add_submenu_page( 'wpmu-admin.php', 'Limit Login Attempts', 'Limit Login Attempts', 9, $this->_options_page_slug, array($this, 'options_page') );
219
-
220
- return;
221
- }
222
-
223
- // Older WP
224
- add_options_page( 'Limit Login Attempts', 'Limit Login Attempts', 9, $this->_options_page_slug, array($this, 'options_page') );
225
- }
226
-
227
- /**
228
- * Get the correct options page URI
229
- *
230
- * @return mixed
231
- */
232
- public function get_options_page_uri() {
233
- return menu_page_url( $this->_options_page_slug, false );
234
- }
235
-
236
- /**
237
- * Get option by name
238
- *
239
- * @param $option_name
240
- * @return null
241
- */
242
- public function get_option( $option_name ) {
243
- return ( isset( $this->_options[ $option_name ] ) ) ? $this->_options[ $option_name ] : null;
244
- }
245
-
246
- /**
247
- * Setup main options
248
- */
249
- public function setup_options() {
250
- $this->update_option_from_db( 'limit_login_client_type', 'client_type' );
251
- $this->update_option_from_db( 'limit_login_allowed_retries', 'allowed_retries' );
252
- $this->update_option_from_db( 'limit_login_lockout_duration', 'lockout_duration' );
253
- $this->update_option_from_db( 'limit_login_valid_duration', 'valid_duration' );
254
- $this->update_option_from_db( 'limit_login_cookies', 'cookies' );
255
- $this->update_option_from_db( 'limit_login_lockout_notify', 'lockout_notify' );
256
- $this->update_option_from_db( 'limit_login_allowed_lockouts', 'allowed_lockouts' );
257
- $this->update_option_from_db( 'limit_login_long_duration', 'long_duration' );
258
- $this->update_option_from_db( 'limit_login_notify_email_after', 'notify_email_after' );
259
-
260
- $this->sanitize_variables();
261
- }
262
-
263
- public function sanitize_variables() {
264
-
265
- $this->sanitize_simple_int( 'allowed_retries' );
266
- $this->sanitize_simple_int( 'lockout_duration' );
267
- $this->sanitize_simple_int( 'valid_duration' );
268
- $this->sanitize_simple_int( 'allowed_lockouts' );
269
- $this->sanitize_simple_int( 'long_duration' );
270
-
271
- $this->_options[ 'cookies' ] = !!$this->get_option( 'cookies' );
272
-
273
- $notify_email_after = max( 1, intval( $this->get_option( 'notify_email_after' ) ) );
274
- $this->_options[ 'notify_email_after' ] = min( $this->get_option( 'allowed_lockouts' ), $notify_email_after );
275
-
276
- $args = explode( ',', $this->get_option( 'lockout_notify' ) );
277
- $args_allowed = explode( ',', LLA_LOCKOUT_NOTIFY_ALLOWED );
278
- $new_args = array();
279
- foreach ( $args as $a ) {
280
- if( in_array( $a, $args_allowed ) ) {
281
- $new_args[] = $a;
282
- }
283
- }
284
- $this->_options[ 'lockout_notify' ] = implode( ',', $new_args );
285
-
286
- if( $this->get_option( 'client_type' ) != LLA_DIRECT_ADDR && $this->get_option( 'client_type' ) != LLA_PROXY_ADDR ) {
287
- $this->_options[ 'client_type' ] = LLA_DIRECT_ADDR;
288
- }
289
- }
290
-
291
- /**
292
- * Make sure the variables make sense -- simple integer
293
- *
294
- * @param $var_name
295
- */
296
- public function sanitize_simple_int( $var_name ) {
297
- $this->_options[ $var_name ] = max( 1, intval( $this->get_option( $var_name ) ) );
298
- }
299
-
300
- /**
301
- * Update options in db from global variables
302
- */
303
- public function update_options() {
304
- update_option( 'limit_login_client_type', $this->get_option( 'client_type' ) );
305
- update_option( 'limit_login_allowed_retries', $this->get_option( 'allowed_retries' ) );
306
- update_option( 'limit_login_lockout_duration', $this->get_option( 'lockout_duration' ) );
307
- update_option( 'limit_login_allowed_lockouts', $this->get_option( 'allowed_lockouts' ) );
308
- update_option( 'limit_login_long_duration', $this->get_option( 'long_duration' ) );
309
- update_option( 'limit_login_valid_duration', $this->get_option( 'valid_duration' ) );
310
- update_option( 'limit_login_lockout_notify', $this->get_option( 'lockout_notify' ) );
311
- update_option( 'limit_login_notify_email_after', $this->get_option( 'notify_email_after' ) );
312
- update_option( 'limit_login_cookies', $this->get_option( 'cookies' ) ? '1' : '0' );
313
- }
314
-
315
- public function update_option_from_db( $option, $var_name ) {
316
- if( false !== ( $option_value = get_option( $option ) ) ) {
317
- $this->_options[ $var_name ] = $option_value;
318
- }
319
- }
320
-
321
- /**
322
- * Action: successful cookie login
323
- *
324
- * Clear any stored user_meta.
325
- *
326
- * Requires WordPress version 3.0.0, not used in previous versions
327
- *
328
- * @param $cookie_elements
329
- * @param $user
330
- */
331
- public function valid_cookie( $cookie_elements, $user ) {
332
- /*
333
- * As all meta values get cached on user load this should not require
334
- * any extra work for the common case of no stored value.
335
- */
336
-
337
- if( get_user_meta( $user->ID, 'limit_login_previous_cookie' ) ) {
338
- delete_user_meta( $user->ID, 'limit_login_previous_cookie' );
339
- }
340
- }
341
-
342
- /**
343
- * Action: failed cookie login (calls limit_login_failed())
344
- *
345
- * @param $cookie_elements
346
- */
347
- public function failed_cookie( $cookie_elements ) {
348
- $this->clear_auth_cookie();
349
-
350
- /*
351
- * Invalid username gets counted every time.
352
- */
353
- $this->limit_login_failed( $cookie_elements[ 'username' ] );
354
- }
355
-
356
- /**
357
- * Action: failed cookie login hash
358
- *
359
- * Make sure same invalid cookie doesn't get counted more than once.
360
- *
361
- * Requires WordPress version 3.0.0, previous versions use limit_login_failed_cookie()
362
- *
363
- * @param $cookie_elements
364
- */
365
- public function failed_cookie_hash( $cookie_elements ) {
366
- $this->clear_auth_cookie();
367
-
368
- /*
369
- * Under some conditions an invalid auth cookie will be used multiple
370
- * times, which results in multiple failed attempts from that one
371
- * cookie.
372
- *
373
- * Unfortunately I've not been able to replicate this consistently and
374
- * thus have not been able to make sure what the exact cause is.
375
- *
376
- * Probably it is because a reload of for example the admin dashboard
377
- * might result in multiple requests from the browser before the invalid
378
- * cookie can be cleard.
379
- *
380
- * Handle this by only counting the first attempt when the exact same
381
- * cookie is attempted for a user.
382
- */
383
-
384
- extract( $cookie_elements, EXTR_OVERWRITE );
385
-
386
- // Check if cookie is for a valid user
387
- $user = get_user_by( 'login', $username );
388
- if( !$user ) {
389
- // "shouldn't happen" for this action
390
- $this->limit_login_failed( $username );
391
-
392
- return;
393
- }
394
-
395
- $previous_cookie = get_user_meta( $user->ID, 'limit_login_previous_cookie', true );
396
- if( $previous_cookie && $previous_cookie == $cookie_elements ) {
397
- // Identical cookies, ignore this attempt
398
- return;
399
- }
400
-
401
- // Store cookie
402
- if( $previous_cookie )
403
- update_user_meta( $user->ID, 'limit_login_previous_cookie', $cookie_elements );
404
- else
405
- add_user_meta( $user->ID, 'limit_login_previous_cookie', $cookie_elements, true );
406
-
407
- $this->limit_login_failed( $username );
408
- }
409
-
410
- /**
411
- * Must be called in plugin_loaded (really early) to make sure we do not allow
412
- * auth cookies while locked out.
413
- */
414
- public function handle_cookies() {
415
- if( $this->is_limit_login_ok() ) {
416
- return;
417
- }
418
-
419
- $this->clear_auth_cookie();
420
- }
421
-
422
- /**
423
- * Check if it is ok to login
424
- *
425
- * @return bool
426
- */
427
- public function is_limit_login_ok() {
428
- $ip = $this->get_address();
429
-
430
- /* Check external whitelist filter */
431
- if( $this->is_ip_whitelisted( $ip ) ) {
432
- return true;
433
- }
434
-
435
- /* lockout active? */
436
- $lockouts = get_option( 'limit_login_lockouts' );
437
-
438
- return ( !is_array( $lockouts ) || !isset( $lockouts[ $ip ] ) || time() >= $lockouts[ $ip ] );
439
- }
440
-
441
- /**
442
- * Make sure auth cookie really get cleared (for this session too)
443
- */
444
- public function clear_auth_cookie() {
445
-
446
- if( !function_exists( 'wp_get_current_user' ) ) {
447
- include( ABSPATH . "wp-includes/pluggable.php" );
448
- }
449
-
450
- wp_clear_auth_cookie();
451
-
452
- if( !empty( $_COOKIE[ AUTH_COOKIE ] ) ) {
453
- $_COOKIE[ AUTH_COOKIE ] = '';
454
- }
455
- if( !empty( $_COOKIE[ SECURE_AUTH_COOKIE ] ) ) {
456
- $_COOKIE[ SECURE_AUTH_COOKIE ] = '';
457
- }
458
- if( !empty( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
459
- $_COOKIE[ LOGGED_IN_COOKIE ] = '';
460
- }
461
- }
462
-
463
- /**
464
- * Action when login attempt failed
465
- *
466
- * Increase nr of retries (if necessary). Reset valid value. Setup
467
- * lockout if nr of retries are above threshold. And more!
468
- *
469
- * A note on external whitelist: retries and statistics are still counted and
470
- * notifications done as usual, but no lockout is done.
471
- *
472
- * @param $username
473
- */
474
- public function limit_login_failed( $username ) {
475
-
476
- $ip = $this->get_address();
477
-
478
- /* if currently locked-out, do not add to retries */
479
- $lockouts = get_option( 'limit_login_lockouts' );
480
- if( !is_array( $lockouts ) ) {
481
- $lockouts = array();
482
- }
483
- if( isset( $lockouts[ $ip ] ) && time() < $lockouts[ $ip ] ) {
484
- return;
485
- }
486
-
487
- /* Get the arrays with retries and retries-valid information */
488
- $retries = get_option( 'limit_login_retries' );
489
- $valid = get_option( 'limit_login_retries_valid' );
490
- if( !is_array( $retries ) ) {
491
- $retries = array();
492
- add_option( 'limit_login_retries', $retries, '', 'no' );
493
- }
494
- if( !is_array( $valid ) ) {
495
- $valid = array();
496
- add_option( 'limit_login_retries_valid', $valid, '', 'no' );
497
- }
498
-
499
- /* Check validity and add one to retries */
500
- if( isset( $retries[ $ip ] ) && isset( $valid[ $ip ] ) && time() < $valid[ $ip ] ) {
501
- $retries[ $ip ]++;
502
- } else {
503
- $retries[ $ip ] = 1;
504
- }
505
- $valid[ $ip ] = time() + $this->get_option( 'valid_duration' );
506
-
507
- /* lockout? */
508
- if( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) != 0 ) {
509
- /*
510
- * Not lockout (yet!)
511
- * Do housecleaning (which also saves retry/valid values).
512
- */
513
- $this->cleanup( $retries, null, $valid );
514
-
515
- return;
516
- }
517
-
518
- /* lockout! */
519
-
520
- $whitelisted = $this->is_ip_whitelisted( $ip );
521
-
522
- $retries_long = $this->get_option( 'allowed_retries' ) * $this->get_option( 'allowed_lockouts' );
523
-
524
- /*
525
- * Note that retries and statistics are still counted and notifications
526
- * done as usual for whitelisted ips , but no lockout is done.
527
- */
528
- if( $whitelisted ) {
529
- if( $retries[ $ip ] >= $retries_long ) {
530
- unset( $retries[ $ip ] );
531
- unset( $valid[ $ip ] );
532
- }
533
- } else {
534
- global $limit_login_just_lockedout;
535
- $limit_login_just_lockedout = true;
536
-
537
- /* setup lockout, reset retries as needed */
538
- if( $retries[ $ip ] >= $retries_long ) {
539
- /* long lockout */
540
- $lockouts[ $ip ] = time() + $this->get_option( 'long_duration' );
541
- unset( $retries[ $ip ] );
542
- unset( $valid[ $ip ] );
543
- } else {
544
- /* normal lockout */
545
- $lockouts[ $ip ] = time() + $this->get_option( 'lockout_duration' );
546
- }
547
- }
548
-
549
- /* do housecleaning and save values */
550
- $this->cleanup( $retries, $lockouts, $valid );
551
-
552
- /* do any notification */
553
- $this->notify( $username );
554
-
555
- /* increase statistics */
556
- $total = get_option( 'limit_login_lockouts_total' );
557
- if( $total === false || !is_numeric( $total ) ) {
558
- add_option( 'limit_login_lockouts_total', 1, '', 'no' );
559
- } else {
560
- update_option( 'limit_login_lockouts_total', $total + 1 );
561
- }
562
- }
563
-
564
- /**
565
- * Handle notification in event of lockout
566
- *
567
- * @param $user
568
- */
569
- public function notify( $user ) {
570
- $args = explode( ',', $this->get_option( 'lockout_notify' ) );
571
-
572
- if( empty( $args ) ) {
573
- return;
574
- }
575
-
576
- foreach ( $args as $mode ) {
577
- switch ( trim( $mode ) ) {
578
- case 'email':
579
- $this->notify_email( $user );
580
- break;
581
- case 'log':
582
- $this->notify_log( $user );
583
- break;
584
- }
585
- }
586
- }
587
-
588
- /**
589
- * Email notification of lockout to admin (if configured)
590
- *
591
- * @param $user
592
- */
593
- public function notify_email( $user ) {
594
- $ip = $this->get_address();
595
- $whitelisted = $this->is_ip_whitelisted( $ip );
596
-
597
- $retries = get_option( 'limit_login_retries' );
598
- if( !is_array( $retries ) ) {
599
- $retries = array();
600
- }
601
-
602
- /* check if we are at the right nr to do notification */
603
- if( isset( $retries[ $ip ] ) && ( ( $retries[ $ip ] / $this->get_option( 'allowed_retries' ) ) % $this->get_option( 'notify_email_after' ) ) != 0 ) {
604
- return;
605
- }
606
-
607
- /* Format message. First current lockout duration */
608
- if( !isset( $retries[ $ip ] ) ) {
609
- /* longer lockout */
610
- $count = $this->get_option( 'allowed_retries' )
611
- * $this->get_option( 'allowed_lockouts' );
612
- $lockouts = $this->get_option( 'allowed_lockouts' );
613
- $time = round( $this->get_option( 'long_duration' ) / 3600 );
614
- $when = sprintf( _n( '%d hour', '%d hours', $time, 'limit-login-attempts-reloaded' ), $time );
615
- } else {
616
- /* normal lockout */
617
- $count = $retries[ $ip ];
618
- $lockouts = floor( $count / $this->get_option( 'allowed_retries' ) );
619
- $time = round( $this->get_option( 'lockout_duration' ) / 60 );
620
- $when = sprintf( _n( '%d minute', '%d minutes', $time, 'limit-login-attempts-reloaded' ), $time );
621
- }
622
-
623
- $blogname = $this->is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
624
-
625
- if( $whitelisted ) {
626
- $subject = sprintf( __( "[%s] Failed login attempts from whitelisted IP"
627
- , 'limit-login-attempts-reloaded' )
628
- , $blogname );
629
- } else {
630
- $subject = sprintf( __( "[%s] Too many failed login attempts"
631
- , 'limit-login-attempts-reloaded' )
632
- , $blogname );
633
- }
634
-
635
- $message = sprintf( __( "%d failed login attempts (%d lockout(s)) from IP: %s"
636
- , 'limit-login-attempts-reloaded' ) . "\r\n\r\n"
637
- , $count, $lockouts, $ip );
638
- if( $user != '' ) {
639
- $message .= sprintf( __( "Last user attempted: %s", 'limit-login-attempts-reloaded' )
640
- . "\r\n\r\n", $user );
641
- }
642
- if( $whitelisted ) {
643
- $message .= __( "IP was NOT blocked because of external whitelist.", 'limit-login-attempts-reloaded' );
644
- } else {
645
- $message .= sprintf( __( "IP was blocked for %s", 'limit-login-attempts-reloaded' ), $when );
646
- }
647
-
648
- $admin_email = $this->is_multisite() ? get_site_option( 'admin_email' ) : get_option( 'admin_email' );
649
-
650
- @wp_mail( $admin_email, $subject, $message );
651
- }
652
-
653
- /**
654
- * Is this WP Multisite?
655
- *
656
- * @return bool
657
- */
658
- public function is_multisite() {
659
- return function_exists( 'get_site_option' ) && function_exists( 'is_multisite' ) && is_multisite();
660
- }
661
-
662
- /**
663
- * Logging of lockout (if configured)
664
- *
665
- * @param $user
666
- */
667
- public function notify_log( $user_login ) {
668
-
669
- if( ! $user_login ) {
670
- return;
671
- }
672
-
673
- $log = $option = get_option( 'limit_login_logged' );
674
- if( !is_array( $log ) ) {
675
- $log = array();
676
- }
677
- $ip = $this->get_address();
678
-
679
- /* can be written much simpler, if you do not mind php warnings */
680
- if( isset( $log[ $ip ] ) ) {
681
- if( isset( $log[ $ip ][ $user_login ] ) ) {
682
-
683
- if( is_array( $log[ $ip ][ $user_login ] ) ) { // For new plugin version
684
- $log[ $ip ][ $user_login ]['counter'] += 1;
685
- } else { // For old plugin version
686
- $temp_counter = $log[ $ip ][ $user_login ];
687
- $log[ $ip ][ $user_login ] = array(
688
- 'counter' => $temp_counter + 1
689
- );
690
- }
691
- } else {
692
- $log[ $ip ][ $user_login ] = array(
693
- 'counter' => 1
694
- );
695
- }
696
- } else {
697
- $log[ $ip ] = array(
698
- $user_login => array(
699
- 'counter' => 1
700
- )
701
- );
702
- }
703
-
704
- $log[ $ip ][ $user_login ]['date'] = time();
705
-
706
- if( $option === false ) {
707
- add_option( 'limit_login_logged', $log, '', 'no' ); /* no autoload */
708
- } else {
709
- update_option( 'limit_login_logged', $log );
710
- }
711
- }
712
-
713
- /**
714
- * Check if IP is whitelisted.
715
- *
716
- * This function allow external ip whitelisting using a filter. Note that it can
717
- * be called multiple times during the login process.
718
- *
719
- * Note that retries and statistics are still counted and notifications
720
- * done as usual for whitelisted ips , but no lockout is done.
721
- *
722
- * Example:
723
- * function my_ip_whitelist($allow, $ip) {
724
- * return ($ip == 'my-ip') ? true : $allow;
725
- * }
726
- * add_filter('limit_login_whitelist_ip', 'my_ip_whitelist', 10, 2);
727
- *
728
- * @param null $ip
729
- * @return bool
730
- */
731
- public function is_ip_whitelisted( $ip = null ) {
732
- if( is_null( $ip ) ) {
733
- $ip = $this->get_address();
734
- }
735
- $whitelisted = apply_filters( 'limit_login_whitelist_ip', false, $ip );
736
-
737
- return ( $whitelisted === true );
738
- }
739
-
740
- /**
741
- * Filter: allow login attempt? (called from wp_authenticate())
742
- *
743
- * @param $user
744
- * @param $password
745
- * @return \WP_Error
746
- */
747
- public function wp_authenticate_user( $user, $password ) {
748
-
749
- if( is_wp_error( $user ) || $this->is_limit_login_ok() ) {
750
- return $user;
751
- }
752
-
753
- global $limit_login_my_error_shown;
754
- $limit_login_my_error_shown = true;
755
-
756
- $error = new WP_Error();
757
- // This error should be the same as in "shake it" filter below
758
- $error->add( 'too_many_retries', $this->error_msg() );
759
-
760
- return $error;
761
- }
762
-
763
- /**
764
- * Filter: add this failure to login page "Shake it!"
765
- *
766
- * @param $error_codes
767
- * @return array
768
- */
769
- public function failure_shake( $error_codes ) {
770
- $error_codes[] = 'too_many_retries';
771
-
772
- return $error_codes;
773
- }
774
-
775
- /**
776
- * Keep track of if user or password are empty, to filter errors correctly
777
- *
778
- * @param $user
779
- * @param $password
780
- */
781
- public function track_credentials( $user, $password ) {
782
- global $limit_login_nonempty_credentials;
783
-
784
- $limit_login_nonempty_credentials = ( !empty( $user ) && !empty( $password ) );
785
- }
786
-
787
- /**
788
- * Should we show errors and messages on this page?
789
- *
790
- * @return bool
791
- */
792
- public function login_show_msg() {
793
- if( isset( $_GET[ 'key' ] ) ) {
794
- /* reset password */
795
- return false;
796
- }
797
-
798
- $action = isset( $_REQUEST[ 'action' ] ) ? $_REQUEST[ 'action' ] : '';
799
-
800
- return ( $action != 'lostpassword' && $action != 'retrievepassword'
801
- && $action != 'resetpass' && $action != 'rp'
802
- && $action != 'register' );
803
- }
804
-
805
- /**
806
- * Construct informative error message
807
- *
808
- * @return string
809
- */
810
- public function error_msg() {
811
- $ip = $this->get_address();
812
- $lockouts = get_option( 'limit_login_lockouts' );
813
-
814
- $msg = __( '<strong>ERROR</strong>: Too many failed login attempts.', 'limit-login-attempts-reloaded' ) . ' ';
815
-
816
- if( !is_array( $lockouts ) || !isset( $lockouts[ $ip ] ) || time() >= $lockouts[ $ip ] ) {
817
- /* Huh? No timeout active? */
818
- $msg .= __( 'Please try again later.', 'limit-login-attempts-reloaded' );
819
-
820
- return $msg;
821
- }
822
-
823
- $when = ceil( ( $lockouts[ $ip ] - time() ) / 60 );
824
- if( $when > 60 ) {
825
- $when = ceil( $when / 60 );
826
- $msg .= sprintf( _n( 'Please try again in %d hour.', 'Please try again in %d hours.', $when, 'limit-login-attempts-reloaded' ), $when );
827
- } else {
828
- $msg .= sprintf( _n( 'Please try again in %d minute.', 'Please try again in %d minutes.', $when, 'limit-login-attempts-reloaded' ), $when );
829
- }
830
-
831
- return $msg;
832
- }
833
-
834
- /**
835
- * Add a message to login page when necessary
836
- */
837
- public function add_error_message() {
838
- global $error, $limit_login_my_error_shown;
839
-
840
- if( !$this->login_show_msg() || $limit_login_my_error_shown ) {
841
- return;
842
- }
843
-
844
- $msg = $this->get_message();
845
-
846
- if( $msg != '' ) {
847
- $limit_login_my_error_shown = true;
848
- $error .= $msg;
849
- }
850
-
851
- return;
852
- }
853
-
854
- /**
855
- * Fix up the error message before showing it
856
- *
857
- * @param $content
858
- * @return string
859
- */
860
- public function fixup_error_messages( $content ) {
861
- global $limit_login_just_lockedout, $limit_login_nonempty_credentials, $limit_login_my_error_shown;
862
-
863
- if( !$this->login_show_msg() ) {
864
- return $content;
865
- }
866
-
867
- /*
868
- * During lockout we do not want to show any other error messages (like
869
- * unknown user or empty password).
870
- */
871
- if( !$this->is_limit_login_ok() && !$limit_login_just_lockedout ) {
872
- return $this->error_msg();
873
- }
874
-
875
- /*
876
- * We want to filter the messages 'Invalid username' and
877
- * 'Invalid password' as that is an information leak regarding user
878
- * account names (prior to WP 2.9?).
879
- *
880
- * Also, if more than one error message, put an extra <br /> tag between
881
- * them.
882
- */
883
- $msgs = explode( "<br />\n", $content );
884
-
885
- if( strlen( end( $msgs ) ) == 0 ) {
886
- /* remove last entry empty string */
887
- array_pop( $msgs );
888
- }
889
-
890
- $count = count( $msgs );
891
- $my_warn_count = $limit_login_my_error_shown ? 1 : 0;
892
-
893
- if( $limit_login_nonempty_credentials && $count > $my_warn_count ) {
894
- /* Replace error message, including ours if necessary */
895
- $content = __( '<strong>ERROR</strong>: Incorrect username or password.', 'limit-login-attempts-reloaded' ) . "<br />\n";
896
-
897
- if( $limit_login_my_error_shown || $this->get_message() ) {
898
- $content .= "<br />\n" . $this->get_message() . "<br />\n";
899
- }
900
-
901
- return $content;
902
- } elseif( $count <= 1 ) {
903
- return $content;
904
- }
905
-
906
- $new = '';
907
- while ( $count-- > 0 ) {
908
- $new .= array_shift( $msgs ) . "<br />\n";
909
- if( $count > 0 ) {
910
- $new .= "<br />\n";
911
- }
912
- }
913
-
914
- return $new;
915
- }
916
-
917
- public function fixup_error_messages_wc( \WP_Error $error ) {
918
- $error->add( 1, __( 'WC Error' ) );
919
- }
920
-
921
- /**
922
- * Return current (error) message to show, if any
923
- *
924
- * @return string
925
- */
926
- public function get_message() {
927
- /* Check external whitelist */
928
- if( $this->is_ip_whitelisted() ) {
929
- return '';
930
- }
931
-
932
- /* Is lockout in effect? */
933
- if( !$this->is_limit_login_ok() ) {
934
- return $this->error_msg();
935
- }
936
-
937
- return $this->retries_remaining_msg();
938
- }
939
-
940
- /**
941
- * Construct retries remaining message
942
- *
943
- * @return string
944
- */
945
- public function retries_remaining_msg() {
946
- $ip = $this->get_address();
947
- $retries = get_option( 'limit_login_retries' );
948
- $valid = get_option( 'limit_login_retries_valid' );
949
-
950
- /* Should we show retries remaining? */
951
-
952
- if( !is_array( $retries ) || !is_array( $valid ) ) {
953
- /* no retries at all */
954
- return '';
955
- }
956
- if( !isset( $retries[ $ip ] ) || !isset( $valid[ $ip ] ) || time() > $valid[ $ip ] ) {
957
- /* no: no valid retries */
958
- return '';
959
- }
960
- if( ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) == 0 ) {
961
- /* no: already been locked out for these retries */
962
- return '';
963
- }
964
-
965
- $remaining = max( ( $this->get_option( 'allowed_retries' ) - ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) ), 0 );
966
-
967
- return sprintf( _n( "<strong>%d</strong> attempt remaining.", "<strong>%d</strong> attempts remaining.", $remaining, 'limit-login-attempts-reloaded' ), $remaining );
968
- }
969
-
970
- /**
971
- * Get correct remote address
972
- *
973
- * @param string $type_name
974
- * @return string
975
- */
976
- public function get_address( $type_name = '' ) {
977
-
978
- if( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
979
- return $_SERVER['HTTP_X_FORWARDED_FOR'];
980
- } elseif( isset( $_SERVER['REMOTE_ADDR'] ) ) {
981
- return $_SERVER['REMOTE_ADDR'];
982
- } else {
983
- return '';
984
- }
985
- }
986
-
987
- /**
988
- * Clean up old lockouts and retries, and save supplied arrays
989
- *
990
- * @param null $retries
991
- * @param null $lockouts
992
- * @param null $valid
993
- */
994
- public function cleanup( $retries = null, $lockouts = null, $valid = null ) {
995
- $now = time();
996
- $lockouts = !is_null( $lockouts ) ? $lockouts : get_option( 'limit_login_lockouts' );
997
-
998
- /* remove old lockouts */
999
- if( is_array( $lockouts ) ) {
1000
- foreach ( $lockouts as $ip => $lockout ) {
1001
- if( $lockout < $now ) {
1002
- unset( $lockouts[ $ip ] );
1003
- }
1004
- }
1005
- update_option( 'limit_login_lockouts', $lockouts );
1006
- }
1007
-
1008
- /* remove retries that are no longer valid */
1009
- $valid = !is_null( $valid ) ? $valid : get_option( 'limit_login_retries_valid' );
1010
- $retries = !is_null( $retries ) ? $retries : get_option( 'limit_login_retries' );
1011
- if( !is_array( $valid ) || !is_array( $retries ) ) {
1012
- return;
1013
- }
1014
-
1015
- foreach ( $valid as $ip => $lockout ) {
1016
- if( $lockout < $now ) {
1017
- unset( $valid[ $ip ] );
1018
- unset( $retries[ $ip ] );
1019
- }
1020
- }
1021
-
1022
- /* go through retries directly, if for some reason they've gone out of sync */
1023
- foreach ( $retries as $ip => $retry ) {
1024
- if( !isset( $valid[ $ip ] ) ) {
1025
- unset( $retries[ $ip ] );
1026
- }
1027
- }
1028
-
1029
- update_option( 'limit_login_retries', $retries );
1030
- update_option( 'limit_login_retries_valid', $valid );
1031
- }
1032
-
1033
- /**
1034
- * Render admin options page
1035
- */
1036
- public function options_page() {
1037
- $this->cleanup();
1038
- include_once( LLA_PLUGIN_DIR . '/views/options-page.php' );
1039
- }
1040
-
1041
- /**
1042
- * Show error message
1043
- *
1044
- * @param $msg
1045
- */
1046
- public function show_error( $msg ) {
1047
- LLA_Helpers::show_error( $msg );
1048
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
 
1050
  }
5
  */
6
  class Limit_Login_Attempts {
7
 
8
+ /**
9
+ * Main plugin options
10
+ * @var array
11
+ */
12
+ public $_options = array(
13
+ /* Are we behind a proxy? */
14
+ 'client_type' => LLA_DIRECT_ADDR,
15
+
16
+ /* Lock out after this many tries */
17
+ 'allowed_retries' => 4,
18
+
19
+ /* Lock out for this many seconds */
20
+ 'lockout_duration' => 1200, // 20 minutes
21
+
22
+ /* Long lock out after this many lockouts */
23
+ 'allowed_lockouts' => 4,
24
+
25
+ /* Long lock out for this many seconds */
26
+ 'long_duration' => 86400, // 24 hours,
27
+
28
+ /* Reset failed attempts after this many seconds */
29
+ 'valid_duration' => 43200, // 12 hours
30
+
31
+ /* Also limit malformed/forged cookies? */
32
+ 'cookies' => true,
33
+
34
+ /* Notify on lockout. Values: '', 'log', 'email', 'log,email' */
35
+ 'lockout_notify' => 'log',
36
+
37
+ /* If notify by email, do so after this number of lockouts */
38
+ 'notify_email_after' => 4,
39
+
40
+ 'whitelist' => array()
41
+ );
42
+
43
+ /**
44
+ * Admin options page slug
45
+ * @var string
46
+ */
47
+ private $_options_page_slug = 'limit-login-attempts';
48
+
49
+ /**
50
+ * Errors messages
51
+ *
52
+ * @var array
53
+ */
54
+ public $_errors = array();
55
+
56
+ public function __construct() {
57
+ $this->hooks_init();
58
+ }
59
+
60
+ /**
61
+ * Register wp hooks and filters
62
+ */
63
+ public function hooks_init() {
64
+ add_action( 'plugins_loaded', array( $this, 'setup' ), 9999 );
65
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
66
+ add_filter( 'limit_login_whitelist_ip', array( $this, 'check_whitelist' ), 10, 2);
67
+ }
68
+
69
+ public function check_whitelist( $allow, $ip ) {
70
+ return in_array( $ip, (array) $this->get_option( 'whitelist' ) );
71
+ }
72
+
73
+ /**
74
+ * @param $error IXR_Error
75
+ *
76
+ * @return IXR_Error
77
+ */
78
+ public function xmlrpc_error_messages( $error ) {
79
+
80
+ if ( ! class_exists( 'IXR_Error' ) ) {
81
+ return $error;
82
+ }
83
+
84
+ if ( ! $this->is_limit_login_ok() ) {
85
+ return new IXR_Error( 403, $this->error_msg() );
86
+ }
87
+
88
+ $ip = $this->get_address();
89
+ $retries = get_option( 'limit_login_retries' );
90
+ $valid = get_option( 'limit_login_retries_valid' );
91
+
92
+ /* Should we show retries remaining? */
93
+
94
+ if ( ! is_array( $retries ) || ! is_array( $valid ) ) {
95
+ /* no retries at all */
96
+ return $error;
97
+ }
98
+ if ( ! isset( $retries[ $ip ] ) || ! isset( $valid[ $ip ] ) || time() > $valid[ $ip ] ) {
99
+ /* no: no valid retries */
100
+ return $error;
101
+ }
102
+ if ( ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) == 0 ) {
103
+ /* no: already been locked out for these retries */
104
+ return $error;
105
+ }
106
+
107
+ $remaining = max( ( $this->get_option( 'allowed_retries' ) - ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) ), 0 );
108
+
109
+ return new IXR_Error( 403, sprintf( _n( "<strong>%d</strong> attempt remaining.", "<strong>%d</strong> attempts remaining.", $remaining, 'limit-login-attempts-reloaded' ), $remaining ) );
110
+ }
111
+
112
+ /**
113
+ * Errors on WooCommerce account page
114
+ */
115
+ public function add_wc_notices() {
116
+
117
+ global $limit_login_just_lockedout, $limit_login_nonempty_credentials, $limit_login_my_error_shown;
118
+
119
+ if ( ! function_exists( 'is_account_page' ) || ! function_exists( 'wc_add_notice' ) ) {
120
+ return;
121
+ }
122
+
123
+ /*
124
+ * During lockout we do not want to show any other error messages (like
125
+ * unknown user or empty password).
126
+ */
127
+ if ( empty( $_POST ) && ! $this->is_limit_login_ok() && ! $limit_login_just_lockedout ) {
128
+ if ( is_account_page() ) {
129
+ wc_add_notice( $this->error_msg(), 'error' );
130
+ }
131
+ }
132
+
133
+ }
134
+
135
+ public function setup() {
136
+ $this->check_original_installed();
137
+
138
+ load_plugin_textdomain( 'limit-login-attempts-reloaded', false, plugin_basename( dirname( __FILE__ ) ) . '/../languages' );
139
+ $this->setup_options();
140
+
141
+ add_action( 'wp_login_failed', array( $this, 'limit_login_failed' ) );
142
+
143
+ // TODO: remove this
144
  // if( $this->get_option( 'cookies' ) ) {
145
  // $this->handle_cookies();
146
  //
156
  // }
157
  // }
158
 
159
+ add_filter( 'wp_authenticate_user', array( $this, 'wp_authenticate_user' ), 99999, 2 );
160
+ add_filter( 'shake_error_codes', array( $this, 'failure_shake' ) );
161
+ add_action( 'login_head', array( $this, 'add_error_message' ) );
162
+ add_action( 'login_errors', array( $this, 'fixup_error_messages' ) );
163
+ add_action( 'admin_menu', array( $this, 'admin_menu' ) );
164
+
165
+ // Add notices for XMLRPC request
166
+ add_filter( 'xmlrpc_login_error', array( $this, 'xmlrpc_error_messages' ) );
167
+
168
+ // Add notices to woocommerce login page
169
+ add_action( 'wp_head', array( $this, 'add_wc_notices' ) );
170
+
171
+ /*
172
+ * This action should really be changed to the 'authenticate' filter as
173
+ * it will probably be deprecated. That is however only available in
174
+ * later versions of WP.
175
+ */
176
+ add_action( 'wp_authenticate', array( $this, 'track_credentials' ), 10, 2 );
177
+ }
178
+
179
+ /**
180
+ * Check if the original plugin is installed
181
+ */
182
+ private function check_original_installed() {
183
+
184
+ if ( defined( 'LIMIT_LOGIN_DIRECT_ADDR' ) ) { // Original plugin is installed
185
+
186
+ if ( $active_plugins = get_option( 'active_plugins' ) ) {
187
+ $deactivate_this = array(
188
+ 'limit-login-attempts-reloaded/limit-login-attempts-reloaded.php'
189
+ );
190
+ $active_plugins = array_diff( $active_plugins, $deactivate_this );
191
+ update_option( 'active_plugins', $active_plugins );
192
+
193
+ add_action( 'admin_notices', function () {
194
+ ?>
195
+ <div class="notice notice-error is-dismissible">
196
+ <p><?php _e( 'Please deactivate the Limit Login Attempts first.', 'limit-login-attempts-reloaded' ); ?></p>
197
+ </div>
198
+ <?php
199
+ } );
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Enqueue js and css
206
+ */
207
+ public function enqueue() {
208
+ wp_enqueue_style( 'lla-main', LLA_PLUGIN_URL . '/assets/css/limit-login-attempts.css' );
209
+ }
210
+
211
+ /**
212
+ * Add admin options page
213
+ */
214
+ public function admin_menu() {
215
+ global $wp_version;
216
+
217
+ // Modern WP?
218
+ if ( version_compare( $wp_version, '3.0', '>=' ) ) {
219
+ add_options_page( 'Limit Login Attempts', 'Limit Login Attempts', 'manage_options', $this->_options_page_slug, array(
220
+ $this,
221
+ 'options_page'
222
+ ) );
223
+
224
+ return;
225
+ }
226
+
227
+ // Older WPMU?
228
+ if ( function_exists( "get_current_site" ) ) {
229
+ add_submenu_page( 'wpmu-admin.php', 'Limit Login Attempts', 'Limit Login Attempts', 9, $this->_options_page_slug, array(
230
+ $this,
231
+ 'options_page'
232
+ ) );
233
+
234
+ return;
235
+ }
236
+
237
+ // Older WP
238
+ add_options_page( 'Limit Login Attempts', 'Limit Login Attempts', 9, $this->_options_page_slug, array(
239
+ $this,
240
+ 'options_page'
241
+ ) );
242
+ }
243
+
244
+ /**
245
+ * Get the correct options page URI
246
+ *
247
+ * @return mixed
248
+ */
249
+ public function get_options_page_uri() {
250
+ return menu_page_url( $this->_options_page_slug, false );
251
+ }
252
+
253
+ /**
254
+ * Get option by name
255
+ *
256
+ * @param $option_name
257
+ *
258
+ * @return null
259
+ */
260
+ public function get_option( $option_name ) {
261
+ return ( isset( $this->_options[ $option_name ] ) ) ? $this->_options[ $option_name ] : null;
262
+ }
263
+
264
+ /**
265
+ * Setup main options
266
+ */
267
+ public function setup_options() {
268
+ $this->update_option_from_db( 'limit_login_client_type', 'client_type' );
269
+ $this->update_option_from_db( 'limit_login_allowed_retries', 'allowed_retries' );
270
+ $this->update_option_from_db( 'limit_login_lockout_duration', 'lockout_duration' );
271
+ $this->update_option_from_db( 'limit_login_valid_duration', 'valid_duration' );
272
+ $this->update_option_from_db( 'limit_login_cookies', 'cookies' );
273
+ $this->update_option_from_db( 'limit_login_lockout_notify', 'lockout_notify' );
274
+ $this->update_option_from_db( 'limit_login_allowed_lockouts', 'allowed_lockouts' );
275
+ $this->update_option_from_db( 'limit_login_long_duration', 'long_duration' );
276
+ $this->update_option_from_db( 'limit_login_notify_email_after', 'notify_email_after' );
277
+ $this->update_option_from_db( 'limit_login_whitelist', 'whitelist' );
278
+
279
+ $this->sanitize_variables();
280
+ }
281
+
282
+ public function sanitize_variables() {
283
+
284
+ $this->sanitize_simple_int( 'allowed_retries' );
285
+ $this->sanitize_simple_int( 'lockout_duration' );
286
+ $this->sanitize_simple_int( 'valid_duration' );
287
+ $this->sanitize_simple_int( 'allowed_lockouts' );
288
+ $this->sanitize_simple_int( 'long_duration' );
289
+
290
+ $this->_options['cookies'] = ! ! $this->get_option( 'cookies' );
291
+
292
+ $notify_email_after = max( 1, intval( $this->get_option( 'notify_email_after' ) ) );
293
+ $this->_options['notify_email_after'] = min( $this->get_option( 'allowed_lockouts' ), $notify_email_after );
294
+
295
+ $args = explode( ',', $this->get_option( 'lockout_notify' ) );
296
+ $args_allowed = explode( ',', LLA_LOCKOUT_NOTIFY_ALLOWED );
297
+ $new_args = array();
298
+ foreach ( $args as $a ) {
299
+ if ( in_array( $a, $args_allowed ) ) {
300
+ $new_args[] = $a;
301
+ }
302
+ }
303
+ $this->_options['lockout_notify'] = implode( ',', $new_args );
304
+
305
+ if ( $this->get_option( 'client_type' ) != LLA_DIRECT_ADDR && $this->get_option( 'client_type' ) != LLA_PROXY_ADDR ) {
306
+ $this->_options['client_type'] = LLA_DIRECT_ADDR;
307
+ }
308
+
309
+ }
310
+
311
+ /**
312
+ * Make sure the variables make sense -- simple integer
313
+ *
314
+ * @param $var_name
315
+ */
316
+ public function sanitize_simple_int( $var_name ) {
317
+ $this->_options[ $var_name ] = max( 1, intval( $this->get_option( $var_name ) ) );
318
+ }
319
+
320
+ /**
321
+ * Update options in db from global variables
322
+ */
323
+ public function update_options() {
324
+ update_option( 'limit_login_client_type', $this->get_option( 'client_type' ) );
325
+ update_option( 'limit_login_allowed_retries', $this->get_option( 'allowed_retries' ) );
326
+ update_option( 'limit_login_lockout_duration', $this->get_option( 'lockout_duration' ) );
327
+ update_option( 'limit_login_allowed_lockouts', $this->get_option( 'allowed_lockouts' ) );
328
+ update_option( 'limit_login_long_duration', $this->get_option( 'long_duration' ) );
329
+ update_option( 'limit_login_valid_duration', $this->get_option( 'valid_duration' ) );
330
+ update_option( 'limit_login_lockout_notify', $this->get_option( 'lockout_notify' ) );
331
+ update_option( 'limit_login_notify_email_after', $this->get_option( 'notify_email_after' ) );
332
+ update_option( 'limit_login_cookies', $this->get_option( 'cookies' ) ? '1' : '0' );
333
+ update_option( 'limit_login_whitelist', $this->get_option( 'whitelist' ) );
334
+ }
335
+
336
+ public function update_option_from_db( $option, $var_name ) {
337
+ if ( false !== ( $option_value = get_option( $option ) ) ) {
338
+ $this->_options[ $var_name ] = $option_value;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Action: successful cookie login
344
+ *
345
+ * Clear any stored user_meta.
346
+ *
347
+ * Requires WordPress version 3.0.0, not used in previous versions
348
+ *
349
+ * @param $cookie_elements
350
+ * @param $user
351
+ */
352
+ public function valid_cookie( $cookie_elements, $user ) {
353
+ /*
354
+ * As all meta values get cached on user load this should not require
355
+ * any extra work for the common case of no stored value.
356
+ */
357
+
358
+ if ( get_user_meta( $user->ID, 'limit_login_previous_cookie' ) ) {
359
+ delete_user_meta( $user->ID, 'limit_login_previous_cookie' );
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Action: failed cookie login (calls limit_login_failed())
365
+ *
366
+ * @param $cookie_elements
367
+ */
368
+ public function failed_cookie( $cookie_elements ) {
369
+ $this->clear_auth_cookie();
370
+
371
+ /*
372
+ * Invalid username gets counted every time.
373
+ */
374
+ $this->limit_login_failed( $cookie_elements['username'] );
375
+ }
376
+
377
+ /**
378
+ * Action: failed cookie login hash
379
+ *
380
+ * Make sure same invalid cookie doesn't get counted more than once.
381
+ *
382
+ * Requires WordPress version 3.0.0, previous versions use limit_login_failed_cookie()
383
+ *
384
+ * @param $cookie_elements
385
+ */
386
+ public function failed_cookie_hash( $cookie_elements ) {
387
+ $this->clear_auth_cookie();
388
+
389
+ /*
390
+ * Under some conditions an invalid auth cookie will be used multiple
391
+ * times, which results in multiple failed attempts from that one
392
+ * cookie.
393
+ *
394
+ * Unfortunately I've not been able to replicate this consistently and
395
+ * thus have not been able to make sure what the exact cause is.
396
+ *
397
+ * Probably it is because a reload of for example the admin dashboard
398
+ * might result in multiple requests from the browser before the invalid
399
+ * cookie can be cleard.
400
+ *
401
+ * Handle this by only counting the first attempt when the exact same
402
+ * cookie is attempted for a user.
403
+ */
404
+
405
+ extract( $cookie_elements, EXTR_OVERWRITE );
406
+
407
+ // Check if cookie is for a valid user
408
+ $user = get_user_by( 'login', $username );
409
+ if ( ! $user ) {
410
+ // "shouldn't happen" for this action
411
+ $this->limit_login_failed( $username );
412
+
413
+ return;
414
+ }
415
+
416
+ $previous_cookie = get_user_meta( $user->ID, 'limit_login_previous_cookie', true );
417
+ if ( $previous_cookie && $previous_cookie == $cookie_elements ) {
418
+ // Identical cookies, ignore this attempt
419
+ return;
420
+ }
421
+
422
+ // Store cookie
423
+ if ( $previous_cookie ) {
424
+ update_user_meta( $user->ID, 'limit_login_previous_cookie', $cookie_elements );
425
+ } else {
426
+ add_user_meta( $user->ID, 'limit_login_previous_cookie', $cookie_elements, true );
427
+ }
428
+
429
+ $this->limit_login_failed( $username );
430
+ }
431
+
432
+ /**
433
+ * Must be called in plugin_loaded (really early) to make sure we do not allow
434
+ * auth cookies while locked out.
435
+ */
436
+ public function handle_cookies() {
437
+ if ( $this->is_limit_login_ok() ) {
438
+ return;
439
+ }
440
+
441
+ $this->clear_auth_cookie();
442
+ }
443
+
444
+ /**
445
+ * Check if it is ok to login
446
+ *
447
+ * @return bool
448
+ */
449
+ public function is_limit_login_ok() {
450
+ $ip = $this->get_address();
451
+
452
+ /* Check external whitelist filter */
453
+ if ( $this->is_ip_whitelisted( $ip ) ) {
454
+ return true;
455
+ }
456
+
457
+ /* lockout active? */
458
+ $lockouts = get_option( 'limit_login_lockouts' );
459
+
460
+ return ( ! is_array( $lockouts ) || ! isset( $lockouts[ $ip ] ) || time() >= $lockouts[ $ip ] );
461
+ }
462
+
463
+ /**
464
+ * Make sure auth cookie really get cleared (for this session too)
465
+ */
466
+ public function clear_auth_cookie() {
467
+
468
+ if ( ! function_exists( 'wp_get_current_user' ) ) {
469
+ include( ABSPATH . "wp-includes/pluggable.php" );
470
+ }
471
+
472
+ wp_clear_auth_cookie();
473
+
474
+ if ( ! empty( $_COOKIE[ AUTH_COOKIE ] ) ) {
475
+ $_COOKIE[ AUTH_COOKIE ] = '';
476
+ }
477
+ if ( ! empty( $_COOKIE[ SECURE_AUTH_COOKIE ] ) ) {
478
+ $_COOKIE[ SECURE_AUTH_COOKIE ] = '';
479
+ }
480
+ if ( ! empty( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
481
+ $_COOKIE[ LOGGED_IN_COOKIE ] = '';
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Action when login attempt failed
487
+ *
488
+ * Increase nr of retries (if necessary). Reset valid value. Setup
489
+ * lockout if nr of retries are above threshold. And more!
490
+ *
491
+ * A note on external whitelist: retries and statistics are still counted and
492
+ * notifications done as usual, but no lockout is done.
493
+ *
494
+ * @param $username
495
+ */
496
+ public function limit_login_failed( $username ) {
497
+
498
+ $ip = $this->get_address();
499
+
500
+ /* if currently locked-out, do not add to retries */
501
+ $lockouts = get_option( 'limit_login_lockouts' );
502
+ if ( ! is_array( $lockouts ) ) {
503
+ $lockouts = array();
504
+ }
505
+ if ( isset( $lockouts[ $ip ] ) && time() < $lockouts[ $ip ] ) {
506
+ return;
507
+ }
508
+
509
+ /* Get the arrays with retries and retries-valid information */
510
+ $retries = get_option( 'limit_login_retries' );
511
+ $valid = get_option( 'limit_login_retries_valid' );
512
+ if ( ! is_array( $retries ) ) {
513
+ $retries = array();
514
+ add_option( 'limit_login_retries', $retries, '', 'no' );
515
+ }
516
+ if ( ! is_array( $valid ) ) {
517
+ $valid = array();
518
+ add_option( 'limit_login_retries_valid', $valid, '', 'no' );
519
+ }
520
+
521
+ /* Check validity and add one to retries */
522
+ if ( isset( $retries[ $ip ] ) && isset( $valid[ $ip ] ) && time() < $valid[ $ip ] ) {
523
+ $retries[ $ip ] ++;
524
+ } else {
525
+ $retries[ $ip ] = 1;
526
+ }
527
+ $valid[ $ip ] = time() + $this->get_option( 'valid_duration' );
528
+
529
+ /* lockout? */
530
+ if ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) != 0 ) {
531
+ /*
532
+ * Not lockout (yet!)
533
+ * Do housecleaning (which also saves retry/valid values).
534
+ */
535
+ $this->cleanup( $retries, null, $valid );
536
+
537
+ return;
538
+ }
539
+
540
+ /* lockout! */
541
+
542
+ $whitelisted = $this->is_ip_whitelisted( $ip );
543
+
544
+ $retries_long = $this->get_option( 'allowed_retries' ) * $this->get_option( 'allowed_lockouts' );
545
+
546
+ /*
547
+ * Note that retries and statistics are still counted and notifications
548
+ * done as usual for whitelisted ips , but no lockout is done.
549
+ */
550
+ if ( $whitelisted ) {
551
+ if ( $retries[ $ip ] >= $retries_long ) {
552
+ unset( $retries[ $ip ] );
553
+ unset( $valid[ $ip ] );
554
+ }
555
+ } else {
556
+ global $limit_login_just_lockedout;
557
+ $limit_login_just_lockedout = true;
558
+
559
+ /* setup lockout, reset retries as needed */
560
+ if ( $retries[ $ip ] >= $retries_long ) {
561
+ /* long lockout */
562
+ $lockouts[ $ip ] = time() + $this->get_option( 'long_duration' );
563
+ unset( $retries[ $ip ] );
564
+ unset( $valid[ $ip ] );
565
+ } else {
566
+ /* normal lockout */
567
+ $lockouts[ $ip ] = time() + $this->get_option( 'lockout_duration' );
568
+ }
569
+ }
570
+
571
+ /* do housecleaning and save values */
572
+ $this->cleanup( $retries, $lockouts, $valid );
573
+
574
+ /* do any notification */
575
+ $this->notify( $username );
576
+
577
+ /* increase statistics */
578
+ $total = get_option( 'limit_login_lockouts_total' );
579
+ if ( $total === false || ! is_numeric( $total ) ) {
580
+ add_option( 'limit_login_lockouts_total', 1, '', 'no' );
581
+ } else {
582
+ update_option( 'limit_login_lockouts_total', $total + 1 );
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Handle notification in event of lockout
588
+ *
589
+ * @param $user
590
+ */
591
+ public function notify( $user ) {
592
+ $args = explode( ',', $this->get_option( 'lockout_notify' ) );
593
+
594
+ if ( empty( $args ) ) {
595
+ return;
596
+ }
597
+
598
+ foreach ( $args as $mode ) {
599
+ switch ( trim( $mode ) ) {
600
+ case 'email':
601
+ $this->notify_email( $user );
602
+ break;
603
+ case 'log':
604
+ $this->notify_log( $user );
605
+ break;
606
+ }
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Email notification of lockout to admin (if configured)
612
+ *
613
+ * @param $user
614
+ */
615
+ public function notify_email( $user ) {
616
+ $ip = $this->get_address();
617
+ $whitelisted = $this->is_ip_whitelisted( $ip );
618
+
619
+ $retries = get_option( 'limit_login_retries' );
620
+ if ( ! is_array( $retries ) ) {
621
+ $retries = array();
622
+ }
623
+
624
+ /* check if we are at the right nr to do notification */
625
+ if ( isset( $retries[ $ip ] ) && ( ( $retries[ $ip ] / $this->get_option( 'allowed_retries' ) ) % $this->get_option( 'notify_email_after' ) ) != 0 ) {
626
+ return;
627
+ }
628
+
629
+ /* Format message. First current lockout duration */
630
+ if ( ! isset( $retries[ $ip ] ) ) {
631
+ /* longer lockout */
632
+ $count = $this->get_option( 'allowed_retries' )
633
+ * $this->get_option( 'allowed_lockouts' );
634
+ $lockouts = $this->get_option( 'allowed_lockouts' );
635
+ $time = round( $this->get_option( 'long_duration' ) / 3600 );
636
+ $when = sprintf( _n( '%d hour', '%d hours', $time, 'limit-login-attempts-reloaded' ), $time );
637
+ } else {
638
+ /* normal lockout */
639
+ $count = $retries[ $ip ];
640
+ $lockouts = floor( $count / $this->get_option( 'allowed_retries' ) );
641
+ $time = round( $this->get_option( 'lockout_duration' ) / 60 );
642
+ $when = sprintf( _n( '%d minute', '%d minutes', $time, 'limit-login-attempts-reloaded' ), $time );
643
+ }
644
+
645
+ $blogname = $this->is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' );
646
+
647
+ if ( $whitelisted ) {
648
+ $subject = sprintf( __( "[%s] Failed login attempts from whitelisted IP"
649
+ , 'limit-login-attempts-reloaded' )
650
+ , $blogname );
651
+ } else {
652
+ $subject = sprintf( __( "[%s] Too many failed login attempts"
653
+ , 'limit-login-attempts-reloaded' )
654
+ , $blogname );
655
+ }
656
+
657
+ $message = sprintf( __( "%d failed login attempts (%d lockout(s)) from IP: %s"
658
+ , 'limit-login-attempts-reloaded' ) . "\r\n\r\n"
659
+ , $count, $lockouts, $ip );
660
+ if ( $user != '' ) {
661
+ $message .= sprintf( __( "Last user attempted: %s", 'limit-login-attempts-reloaded' )
662
+ . "\r\n\r\n", $user );
663
+ }
664
+ if ( $whitelisted ) {
665
+ $message .= __( "IP was NOT blocked because of external whitelist.", 'limit-login-attempts-reloaded' );
666
+ } else {
667
+ $message .= sprintf( __( "IP was blocked for %s", 'limit-login-attempts-reloaded' ), $when );
668
+ }
669
+
670
+ $admin_email = $this->is_multisite() ? get_site_option( 'admin_email' ) : get_option( 'admin_email' );
671
+
672
+ @wp_mail( $admin_email, $subject, $message );
673
+ }
674
+
675
+ /**
676
+ * Is this WP Multisite?
677
+ *
678
+ * @return bool
679
+ */
680
+ public function is_multisite() {
681
+ return function_exists( 'get_site_option' ) && function_exists( 'is_multisite' ) && is_multisite();
682
+ }
683
+
684
+ /**
685
+ * Logging of lockout (if configured)
686
+ *
687
+ * @param $user_login
688
+ *
689
+ * @internal param $user
690
+ */
691
+ public function notify_log( $user_login ) {
692
+
693
+ if ( ! $user_login ) {
694
+ return;
695
+ }
696
+
697
+ $log = $option = get_option( 'limit_login_logged' );
698
+ if ( ! is_array( $log ) ) {
699
+ $log = array();
700
+ }
701
+ $ip = $this->get_address();
702
+
703
+ /* can be written much simpler, if you do not mind php warnings */
704
+ if ( isset( $log[ $ip ] ) ) {
705
+ if ( isset( $log[ $ip ][ $user_login ] ) ) {
706
+
707
+ if ( is_array( $log[ $ip ][ $user_login ] ) ) { // For new plugin version
708
+ $log[ $ip ][ $user_login ]['counter'] += 1;
709
+ } else { // For old plugin version
710
+ $temp_counter = $log[ $ip ][ $user_login ];
711
+ $log[ $ip ][ $user_login ] = array(
712
+ 'counter' => $temp_counter + 1
713
+ );
714
+ }
715
+ } else {
716
+ $log[ $ip ][ $user_login ] = array(
717
+ 'counter' => 1
718
+ );
719
+ }
720
+ } else {
721
+ $log[ $ip ] = array(
722
+ $user_login => array(
723
+ 'counter' => 1
724
+ )
725
+ );
726
+ }
727
+
728
+ $log[ $ip ][ $user_login ]['date'] = time();
729
+
730
+ $gateway = '';
731
+ if( isset( $_POST['woocommerce-login-nonce'] ) ) {
732
+ $gateway = 'WooCommerce';
733
+ } elseif( isset( $GLOBALS['wp_xmlrpc_server'] ) && is_object( $GLOBALS['wp_xmlrpc_server'] ) ) {
734
+ $gateway = 'XMLRPC';
735
+ } else {
736
+ $gateway = 'WP Login';
737
+ }
738
+
739
+ $log[ $ip ][ $user_login ]['gateway'] = $gateway;
740
+
741
+ if ( $option === false ) {
742
+ add_option( 'limit_login_logged', $log, '', 'no' ); /* no autoload */
743
+ } else {
744
+ update_option( 'limit_login_logged', $log );
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Check if IP is whitelisted.
750
+ *
751
+ * This function allow external ip whitelisting using a filter. Note that it can
752
+ * be called multiple times during the login process.
753
+ *
754
+ * Note that retries and statistics are still counted and notifications
755
+ * done as usual for whitelisted ips , but no lockout is done.
756
+ *
757
+ * Example:
758
+ * function my_ip_whitelist($allow, $ip) {
759
+ * return ($ip == 'my-ip') ? true : $allow;
760
+ * }
761
+ * add_filter('limit_login_whitelist_ip', 'my_ip_whitelist', 10, 2);
762
+ *
763
+ * @param null $ip
764
+ *
765
+ * @return bool
766
+ */
767
+ public function is_ip_whitelisted( $ip = null ) {
768
+ if ( is_null( $ip ) ) {
769
+ $ip = $this->get_address();
770
+ }
771
+ $whitelisted = apply_filters( 'limit_login_whitelist_ip', false, $ip );
772
+
773
+ return ( $whitelisted === true );
774
+ }
775
+
776
+ /**
777
+ * Filter: allow login attempt? (called from wp_authenticate())
778
+ *
779
+ * @param $user
780
+ * @param $password
781
+ *
782
+ * @return \WP_Error
783
+ */
784
+ public function wp_authenticate_user( $user, $password ) {
785
+
786
+ if ( is_wp_error( $user ) || $this->is_limit_login_ok() ) {
787
+ return $user;
788
+ }
789
+
790
+ global $limit_login_my_error_shown;
791
+ $limit_login_my_error_shown = true;
792
+
793
+ $error = new WP_Error();
794
+ // This error should be the same as in "shake it" filter below
795
+ $error->add( 'too_many_retries', $this->error_msg() );
796
+
797
+ return $error;
798
+ }
799
+
800
+ /**
801
+ * Filter: add this failure to login page "Shake it!"
802
+ *
803
+ * @param $error_codes
804
+ *
805
+ * @return array
806
+ */
807
+ public function failure_shake( $error_codes ) {
808
+ $error_codes[] = 'too_many_retries';
809
+
810
+ return $error_codes;
811
+ }
812
+
813
+ /**
814
+ * Keep track of if user or password are empty, to filter errors correctly
815
+ *
816
+ * @param $user
817
+ * @param $password
818
+ */
819
+ public function track_credentials( $user, $password ) {
820
+ global $limit_login_nonempty_credentials;
821
+
822
+ $limit_login_nonempty_credentials = ( ! empty( $user ) && ! empty( $password ) );
823
+ }
824
+
825
+ /**
826
+ * Should we show errors and messages on this page?
827
+ *
828
+ * @return bool
829
+ */
830
+ public function login_show_msg() {
831
+ if ( isset( $_GET['key'] ) ) {
832
+ /* reset password */
833
+ return false;
834
+ }
835
+
836
+ $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
837
+
838
+ return ( $action != 'lostpassword' && $action != 'retrievepassword'
839
+ && $action != 'resetpass' && $action != 'rp'
840
+ && $action != 'register' );
841
+ }
842
+
843
+ /**
844
+ * Construct informative error message
845
+ *
846
+ * @return string
847
+ */
848
+ public function error_msg() {
849
+ $ip = $this->get_address();
850
+ $lockouts = get_option( 'limit_login_lockouts' );
851
+
852
+ $msg = __( '<strong>ERROR</strong>: Too many failed login attempts.', 'limit-login-attempts-reloaded' ) . ' ';
853
+
854
+ if ( ! is_array( $lockouts ) || ! isset( $lockouts[ $ip ] ) || time() >= $lockouts[ $ip ] ) {
855
+ /* Huh? No timeout active? */
856
+ $msg .= __( 'Please try again later.', 'limit-login-attempts-reloaded' );
857
+
858
+ return $msg;
859
+ }
860
+
861
+ $when = ceil( ( $lockouts[ $ip ] - time() ) / 60 );
862
+ if ( $when > 60 ) {
863
+ $when = ceil( $when / 60 );
864
+ $msg .= sprintf( _n( 'Please try again in %d hour.', 'Please try again in %d hours.', $when, 'limit-login-attempts-reloaded' ), $when );
865
+ } else {
866
+ $msg .= sprintf( _n( 'Please try again in %d minute.', 'Please try again in %d minutes.', $when, 'limit-login-attempts-reloaded' ), $when );
867
+ }
868
+
869
+ return $msg;
870
+ }
871
+
872
+ /**
873
+ * Add a message to login page when necessary
874
+ */
875
+ public function add_error_message() {
876
+ global $error, $limit_login_my_error_shown;
877
+
878
+ if ( ! $this->login_show_msg() || $limit_login_my_error_shown ) {
879
+ return;
880
+ }
881
+
882
+ $msg = $this->get_message();
883
+
884
+ if ( $msg != '' ) {
885
+ $limit_login_my_error_shown = true;
886
+ $error .= $msg;
887
+ }
888
+
889
+ return;
890
+ }
891
+
892
+ /**
893
+ * Fix up the error message before showing it
894
+ *
895
+ * @param $content
896
+ *
897
+ * @return string
898
+ */
899
+ public function fixup_error_messages( $content ) {
900
+ global $limit_login_just_lockedout, $limit_login_nonempty_credentials, $limit_login_my_error_shown;
901
+
902
+ if ( ! $this->login_show_msg() ) {
903
+ return $content;
904
+ }
905
+
906
+ /*
907
+ * During lockout we do not want to show any other error messages (like
908
+ * unknown user or empty password).
909
+ */
910
+ if ( ! $this->is_limit_login_ok() && ! $limit_login_just_lockedout ) {
911
+ return $this->error_msg();
912
+ }
913
+
914
+ /*
915
+ * We want to filter the messages 'Invalid username' and
916
+ * 'Invalid password' as that is an information leak regarding user
917
+ * account names (prior to WP 2.9?).
918
+ *
919
+ * Also, if more than one error message, put an extra <br /> tag between
920
+ * them.
921
+ */
922
+ $msgs = explode( "<br />\n", $content );
923
+
924
+ if ( strlen( end( $msgs ) ) == 0 ) {
925
+ /* remove last entry empty string */
926
+ array_pop( $msgs );
927
+ }
928
+
929
+ $count = count( $msgs );
930
+ $my_warn_count = $limit_login_my_error_shown ? 1 : 0;
931
+
932
+ if ( $limit_login_nonempty_credentials && $count > $my_warn_count ) {
933
+ /* Replace error message, including ours if necessary */
934
+ $content = __( '<strong>ERROR</strong>: Incorrect username or password.', 'limit-login-attempts-reloaded' ) . "<br />\n";
935
+
936
+ if ( $limit_login_my_error_shown || $this->get_message() ) {
937
+ $content .= "<br />\n" . $this->get_message() . "<br />\n";
938
+ }
939
+
940
+ return $content;
941
+ } elseif ( $count <= 1 ) {
942
+ return $content;
943
+ }
944
+
945
+ $new = '';
946
+ while ( $count -- > 0 ) {
947
+ $new .= array_shift( $msgs ) . "<br />\n";
948
+ if ( $count > 0 ) {
949
+ $new .= "<br />\n";
950
+ }
951
+ }
952
+
953
+ return $new;
954
+ }
955
+
956
+ public function fixup_error_messages_wc( \WP_Error $error ) {
957
+ $error->add( 1, __( 'WC Error' ) );
958
+ }
959
+
960
+ /**
961
+ * Return current (error) message to show, if any
962
+ *
963
+ * @return string
964
+ */
965
+ public function get_message() {
966
+ /* Check external whitelist */
967
+ if ( $this->is_ip_whitelisted() ) {
968
+ return '';
969
+ }
970
+
971
+ /* Is lockout in effect? */
972
+ if ( ! $this->is_limit_login_ok() ) {
973
+ return $this->error_msg();
974
+ }
975
+
976
+ return $this->retries_remaining_msg();
977
+ }
978
+
979
+ /**
980
+ * Construct retries remaining message
981
+ *
982
+ * @return string
983
+ */
984
+ public function retries_remaining_msg() {
985
+ $ip = $this->get_address();
986
+ $retries = get_option( 'limit_login_retries' );
987
+ $valid = get_option( 'limit_login_retries_valid' );
988
+
989
+ /* Should we show retries remaining? */
990
+
991
+ if ( ! is_array( $retries ) || ! is_array( $valid ) ) {
992
+ /* no retries at all */
993
+ return '';
994
+ }
995
+ if ( ! isset( $retries[ $ip ] ) || ! isset( $valid[ $ip ] ) || time() > $valid[ $ip ] ) {
996
+ /* no: no valid retries */
997
+ return '';
998
+ }
999
+ if ( ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) == 0 ) {
1000
+ /* no: already been locked out for these retries */
1001
+ return '';
1002
+ }
1003
+
1004
+ $remaining = max( ( $this->get_option( 'allowed_retries' ) - ( $retries[ $ip ] % $this->get_option( 'allowed_retries' ) ) ), 0 );
1005
+
1006
+ return sprintf( _n( "<strong>%d</strong> attempt remaining.", "<strong>%d</strong> attempts remaining.", $remaining, 'limit-login-attempts-reloaded' ), $remaining );
1007
+ }
1008
+
1009
+ /**
1010
+ * Get correct remote address
1011
+ *
1012
+ * @param string $type_name
1013
+ *
1014
+ * @return string
1015
+ */
1016
+ public function get_address( $type_name = '' ) {
1017
+
1018
+ if ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
1019
+ return $_SERVER['HTTP_X_FORWARDED_FOR'];
1020
+ } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
1021
+ return $_SERVER['REMOTE_ADDR'];
1022
+ } else {
1023
+ return '';
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Clean up old lockouts and retries, and save supplied arrays
1029
+ *
1030
+ * @param null $retries
1031
+ * @param null $lockouts
1032
+ * @param null $valid
1033
+ */
1034
+ public function cleanup( $retries = null, $lockouts = null, $valid = null ) {
1035
+ $now = time();
1036
+ $lockouts = ! is_null( $lockouts ) ? $lockouts : get_option( 'limit_login_lockouts' );
1037
+
1038
+ /* remove old lockouts */
1039
+ if ( is_array( $lockouts ) ) {
1040
+ foreach ( $lockouts as $ip => $lockout ) {
1041
+ if ( $lockout < $now ) {
1042
+ unset( $lockouts[ $ip ] );
1043
+ }
1044
+ }
1045
+ update_option( 'limit_login_lockouts', $lockouts );
1046
+ }
1047
+
1048
+ /* remove retries that are no longer valid */
1049
+ $valid = ! is_null( $valid ) ? $valid : get_option( 'limit_login_retries_valid' );
1050
+ $retries = ! is_null( $retries ) ? $retries : get_option( 'limit_login_retries' );
1051
+ if ( ! is_array( $valid ) || ! is_array( $retries ) ) {
1052
+ return;
1053
+ }
1054
+
1055
+ foreach ( $valid as $ip => $lockout ) {
1056
+ if ( $lockout < $now ) {
1057
+ unset( $valid[ $ip ] );
1058
+ unset( $retries[ $ip ] );
1059
+ }
1060
+ }
1061
+
1062
+ /* go through retries directly, if for some reason they've gone out of sync */
1063
+ foreach ( $retries as $ip => $retry ) {
1064
+ if ( ! isset( $valid[ $ip ] ) ) {
1065
+ unset( $retries[ $ip ] );
1066
+ }
1067
+ }
1068
+
1069
+ update_option( 'limit_login_retries', $retries );
1070
+ update_option( 'limit_login_retries_valid', $valid );
1071
+ }
1072
+
1073
+ /**
1074
+ * Render admin options page
1075
+ */
1076
+ public function options_page() {
1077
+ $this->cleanup();
1078
+ include_once( LLA_PLUGIN_DIR . '/views/options-page.php' );
1079
+ }
1080
+
1081
+ /**
1082
+ * Show error message
1083
+ *
1084
+ * @param $msg
1085
+ */
1086
+ public function show_error( $msg ) {
1087
+ LLA_Helpers::show_error( $msg );
1088
+ }
1089
 
1090
  }
limit-login-attempts-reloaded.php CHANGED
@@ -4,7 +4,7 @@
4
  Description: Limit the rate of login attempts, including by way of cookies and for each IP address.
5
  Author: wpchefgadget
6
  Text Domain: limit-login-attempts-reloaded
7
- Version: 2.2.0
8
 
9
  Copyright 2008 - 2012 Johan Eenfeldt
10
 
4
  Description: Limit the rate of login attempts, including by way of cookies and for each IP address.
5
  Author: wpchefgadget
6
  Text Domain: limit-login-attempts-reloaded
7
+ Version: 2.3.0
8
 
9
  Copyright 2008 - 2012 Johan Eenfeldt
10
 
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: wpchefgadget
3
  Tags: login, security, authentication, Limit Login Attempts, Limit Login Attempts Reloaded, Limit Login Attempts Revamped, Limit Login Attempts Renovated, Limit Login Attempts Updated, Better Limit Login Attempts, Limit Login Attempts Renewed, Limit Login Attempts Upgraded
4
  Requires at least: 2.8
5
  Tested up to: 4.6.1
6
- Stable tag: 2.2.0
7
 
8
  Reloaded version of the original Limit Login Attempts plugin for Login Protection by a team of WordPress developers.
9
 
@@ -48,6 +48,11 @@ Based on the original code from Limit Login Attemps plugin by Johan Eenfeldt.
48
 
49
  == Changelog ==
50
 
 
 
 
 
 
51
  = 2.2.0 =
52
  * Removed the "Handle cookie login" setting as they are now obsolete.
53
  * Added bruteforce protection against Woocommerce login page attacks. https://wordpress.org/support/topic/how-to-integrate-with-woocommerce-2/
3
  Tags: login, security, authentication, Limit Login Attempts, Limit Login Attempts Reloaded, Limit Login Attempts Revamped, Limit Login Attempts Renovated, Limit Login Attempts Updated, Better Limit Login Attempts, Limit Login Attempts Renewed, Limit Login Attempts Upgraded
4
  Requires at least: 2.8
5
  Tested up to: 4.6.1
6
+ Stable tag: 2.3.0
7
 
8
  Reloaded version of the original Limit Login Attempts plugin for Login Protection by a team of WordPress developers.
9
 
48
 
49
  == Changelog ==
50
 
51
+ = 2.3.0 =
52
+ * IP addresses can be white-listed now. https://wordpress.org/support/topic/legal-user/
53
+ * A "Gateway" column is added to the lockouts log. It shows what endpoint an attacker was blocked from. https://wordpress.org/support/topic/xmlrpc-7/
54
+ * The "Undefined index: client_type" error is fixed. https://wordpress.org/support/topic/php-notice-when-updating-settings-page/
55
+
56
  = 2.2.0 =
57
  * Removed the "Handle cookie login" setting as they are now obsolete.
58
  * Added bruteforce protection against Woocommerce login page attacks. https://wordpress.org/support/topic/how-to-integrate-with-woocommerce-2/
views/options-page.php CHANGED
@@ -37,7 +37,6 @@ if( isset( $_POST[ 'reset_current' ] ) ) {
37
  /* Should we update options? */
38
  if( isset( $_POST[ 'update_options' ] ) ) {
39
 
40
- $this->_options[ 'client_type' ] = $_POST[ 'client_type' ];
41
  $this->_options[ 'allowed_retries' ] = $_POST[ 'allowed_retries' ];
42
  $this->_options[ 'lockout_duration' ] = $_POST[ 'lockout_duration' ] * 60;
43
  $this->_options[ 'valid_duration' ] = $_POST[ 'valid_duration' ] * 3600;
@@ -45,6 +44,18 @@ if( isset( $_POST[ 'update_options' ] ) ) {
45
  $this->_options[ 'long_duration' ] = $_POST[ 'long_duration' ] * 3600;
46
  $this->_options[ 'notify_email_after' ] = $_POST[ 'email_after' ];
47
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  $notify_methods = array();
49
  if( isset( $_POST[ 'lockout_notify_log' ] ) ) {
50
  $notify_methods[] = 'log';
@@ -67,6 +78,9 @@ $lockouts_now = is_array( $lockouts ) ? count( $lockouts ) : 0;
67
  $v = explode( ',', $this->get_option( 'lockout_notify' ) );
68
  $log_checked = in_array( 'log', $v ) ? ' checked ' : '';
69
  $email_checked = in_array( 'email', $v ) ? ' checked ' : '';
 
 
 
70
  ?>
71
  <div class="wrap">
72
  <h2><?php echo __( 'Limit Login Attempts Settings', 'limit-login-attempts-reloaded' ); ?></h2>
@@ -140,6 +154,14 @@ $email_checked = in_array( 'email', $v ) ? ' checked ' : '';
140
  name="email_after"/> <?php echo __( 'lockouts', 'limit-login-attempts-reloaded' ); ?>
141
  </td>
142
  </tr>
 
 
 
 
 
 
 
 
143
  </table>
144
  <p class="submit">
145
  <input class="button button-primary" name="update_options" value="<?php echo __( 'Change Options', 'limit-login-attempts-reloaded' ); ?>"
@@ -166,6 +188,7 @@ $email_checked = in_array( 'email', $v ) ? ' checked ' : '';
166
  <th scope="col"><?php _e( "Date", 'limit-login-attempts-reloaded' ); ?></th>
167
  <th scope="col"><?php echo _x( "IP", "Internet address", 'limit-login-attempts-reloaded' ); ?></th>
168
  <th scope="col"><?php _e( 'Tried to log in as', 'limit-login-attempts-reloaded' ); ?></th>
 
169
  </tr>
170
 
171
  <?php foreach ( $log as $ip => $users ) : ?>
@@ -177,10 +200,12 @@ $email_checked = in_array( 'email', $v ) ? ' checked ' : '';
177
  <td class="limit-login-date"><?php echo date_i18n( 'F d, Y H:i', $info['date'] ); ?></td>
178
  <td class="limit-login-ip"><?php echo $ip; ?></td>
179
  <td class="limit-login-max"><?php echo $user_name . ' (' . $info['counter'] .' lockouts)'; ?></td>
 
180
  <?php else : // For old plugin version ?>
181
  <td class="limit-login-date"></td>
182
  <td class="limit-login-ip"><?php echo $ip; ?></td>
183
  <td class="limit-login-max"><?php echo $user_name . ' (' . $info .' lockouts)'; ?></td>
 
184
  <?php endif; ?>
185
  </tr>
186
  <?php endforeach; ?>
37
  /* Should we update options? */
38
  if( isset( $_POST[ 'update_options' ] ) ) {
39
 
 
40
  $this->_options[ 'allowed_retries' ] = $_POST[ 'allowed_retries' ];
41
  $this->_options[ 'lockout_duration' ] = $_POST[ 'lockout_duration' ] * 60;
42
  $this->_options[ 'valid_duration' ] = $_POST[ 'valid_duration' ] * 3600;
44
  $this->_options[ 'long_duration' ] = $_POST[ 'long_duration' ] * 3600;
45
  $this->_options[ 'notify_email_after' ] = $_POST[ 'email_after' ];
46
 
47
+ $white_list = ( !empty( $_POST['lla_whitelist'] ) ) ? explode("\n", str_replace("\r", "", $_POST['lla_whitelist'] ) ) : array();
48
+
49
+ if( !empty( $white_list ) ) {
50
+ foreach( $white_list as $key => $ip ) {
51
+ if( '' == $ip ) {
52
+ unset( $white_list[ $key ] );
53
+ }
54
+ }
55
+ }
56
+
57
+ $this->_options['whitelist'] = $white_list;
58
+
59
  $notify_methods = array();
60
  if( isset( $_POST[ 'lockout_notify_log' ] ) ) {
61
  $notify_methods[] = 'log';
78
  $v = explode( ',', $this->get_option( 'lockout_notify' ) );
79
  $log_checked = in_array( 'log', $v ) ? ' checked ' : '';
80
  $email_checked = in_array( 'email', $v ) ? ' checked ' : '';
81
+
82
+ $white_list = $this->get_option( 'whitelist' );
83
+ $white_list = ( is_array( $white_list ) && !empty( $white_list ) ) ? implode( "\n", $white_list ) : '';
84
  ?>
85
  <div class="wrap">
86
  <h2><?php echo __( 'Limit Login Attempts Settings', 'limit-login-attempts-reloaded' ); ?></h2>
154
  name="email_after"/> <?php echo __( 'lockouts', 'limit-login-attempts-reloaded' ); ?>
155
  </td>
156
  </tr>
157
+ <tr>
158
+ <th scope="row"
159
+ valign="top"><?php echo __( 'Whitelist (IP)', 'limit-login-attempts-reloaded' ); ?></th>
160
+ <td>
161
+ <p class="description"><?php _e( 'One IP per line.', 'limit-login-attempts-reloaded' ); ?></p>
162
+ <textarea name="lla_whitelist" rows="10" cols="50"><?php echo $white_list; ?></textarea>
163
+ </td>
164
+ </tr>
165
  </table>
166
  <p class="submit">
167
  <input class="button button-primary" name="update_options" value="<?php echo __( 'Change Options', 'limit-login-attempts-reloaded' ); ?>"
188
  <th scope="col"><?php _e( "Date", 'limit-login-attempts-reloaded' ); ?></th>
189
  <th scope="col"><?php echo _x( "IP", "Internet address", 'limit-login-attempts-reloaded' ); ?></th>
190
  <th scope="col"><?php _e( 'Tried to log in as', 'limit-login-attempts-reloaded' ); ?></th>
191
+ <th scope="col"><?php _e( 'Gateway', 'limit-login-attempts-reloaded' ); ?></th>
192
  </tr>
193
 
194
  <?php foreach ( $log as $ip => $users ) : ?>
200
  <td class="limit-login-date"><?php echo date_i18n( 'F d, Y H:i', $info['date'] ); ?></td>
201
  <td class="limit-login-ip"><?php echo $ip; ?></td>
202
  <td class="limit-login-max"><?php echo $user_name . ' (' . $info['counter'] .' lockouts)'; ?></td>
203
+ <td class="limit-login-gateway"><?php echo ( isset( $info['gateway'] ) && !empty( $info['gateway'] ) ) ? $info['gateway'] : '-'; ?></td>
204
  <?php else : // For old plugin version ?>
205
  <td class="limit-login-date"></td>
206
  <td class="limit-login-ip"><?php echo $ip; ?></td>
207
  <td class="limit-login-max"><?php echo $user_name . ' (' . $info .' lockouts)'; ?></td>
208
+ <td class="limit-login-gateway">-</td>
209
  <?php endif; ?>
210
  </tr>
211
  <?php endforeach; ?>