PluginProbe ʕ •ᴥ•ʔ
Admin and Site Enhancements (ASE) / 9.0.2
Admin and Site Enhancements (ASE) v9.0.2
9.1.1 9.1.0 9.0.2 9.0.1 9.0.0 8.9.2 8.9.1 8.9.0 8.8.8 8.8.7 8.8.6 8.8.5 8.8.4 8.8.3 8.8.2 8.8.1 8.8.0 8.7.3 8.7.2 8.7.1 8.2.1 8.2.2 8.2.3 8.3.0 8.3.1 8.3.2 8.4.0 8.4.1 8.4.2 8.5.0 8.5.1 8.5.2 8.6.0 8.6.1 8.6.2 8.7.0 5.0.1 5.0.2 5.0.2.1 5.0.2.2 5.0.2.3 5.0.2.4 5.1.0 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.8.0 5.8.1 6.0.0 6.0.3 6.0.4 6.0.5 6.0.5.1 6.0.6 6.0.7 6.0.8.1 6.1.0 6.1.3 6.2.0 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.7 6.3.0 6.3.1 6.3.2 6.4.0 6.5.0 6.5.1 6.6.0 6.7.0 6.8.0 6.8.2 6.8.3 6.9.0 6.9.1 6.9.10 6.9.11 6.9.12 6.9.13.1 6.9.13.2 6.9.2 6.9.3 6.9.4 6.9.5 6.9.6 6.9.7 6.9.8 6.9.9 7.0.0 7.0.2 7.0.2.1 7.0.2.2 7.0.2.3 7.0.3 7.1.0 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2.0 7.2.1 7.3.0 7.3.1 7.3.2 7.3.3 7.4.0 7.4.2 7.4.4 7.4.5 7.4.6 7.4.7 7.4.8 7.5.0 7.5.1 7.5.2 7.5.3 7.5.4 7.6.0 7.6.1 7.6.1.1 7.6.10 7.6.11 7.6.2 7.6.3 7.6.4 7.6.5 7.6.6 7.6.7 7.6.7.1 7.6.8 7.6.9 7.7.0 7.8.0 7.8.1 7.8.10 7.8.11 7.8.12 7.8.13 7.8.14 7.8.15 7.8.16 7.8.17 7.8.18 7.8.2 7.8.3 7.8.4 7.8.5 7.8.5.1 7.8.6 7.8.7 7.8.8 7.8.9 7.9.0 7.9.1 7.9.10 7.9.11 7.9.2 7.9.3 7.9.4 7.9.5 7.9.6 7.9.7 7.9.8 7.9.9 8.0.0 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.0.6 8.0.7 8.0.8 8.1.0 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 2.1.0 2.2.0 2.3.0 2.4.0 2.5.0 2.6.0 2.7.0 2.8.0 2.8.1 2.8.2 2.8.3 2.9.0 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.2.0 3.3.0 3.4.0 3.5.0 3.6.1 3.7.0 3.8.0 3.9.0 3.9.1 3.9.2 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.1 4.8.2 4.8.3 4.9.0 4.9.1 4.9.2 4.9.3 5.0.0
admin-site-enhancements / classes / class-limit-login-attempts.php
admin-site-enhancements / classes Last commit date
class-activation.php 2 weeks ago class-admin-menu-organizer.php 2 weeks ago class-admin-menu-svg-icon-mask.php 2 weeks ago class-auto-publish-posts-with-missed-schedule.php 2 weeks ago class-avif-upload.php 2 weeks ago class-captcha-protection.php 2 weeks ago class-change-login-url.php 2 weeks ago class-cleanup-admin-bar.php 2 weeks ago class-common-methods.php 2 weeks ago class-content-duplication.php 2 weeks ago class-content-order.php 2 weeks ago class-custom-admin-footer-text.php 2 weeks ago class-custom-body-class.php 2 weeks ago class-custom-css.php 2 weeks ago class-custom-nav-menu-items-in-new-tab.php 2 weeks ago class-deactivation.php 2 weeks ago class-disable-author-archives.php 2 weeks ago class-disable-comments.php 2 weeks ago class-disable-dashboard-widgets.php 2 weeks ago class-disable-embeds.php 2 weeks ago class-disable-feeds.php 2 weeks ago class-disable-gutenberg.php 2 weeks ago class-disable-rest-api.php 2 weeks ago class-disable-smaller-components.php 2 weeks ago class-disable-updates.php 2 weeks ago class-disable-user-account.php 2 weeks ago class-disable-xml-rpc.php 2 weeks ago class-display-system-summary.php 2 weeks ago class-email-address-obfuscator.php 2 weeks ago class-email-delivery.php 2 weeks ago class-enhance-list-tables.php 2 weeks ago class-external-permalinks.php 2 weeks ago class-heartbeat-control.php 2 weeks ago class-hide-admin-bar.php 2 weeks ago class-hide-admin-notices.php 2 weeks ago class-image-sizes-panel.php 2 weeks ago class-image-upload-control.php 2 weeks ago class-insert-head-body-footer-code.php 2 weeks ago class-last-login-column.php 2 weeks ago class-limit-login-attempts.php 2 weeks ago class-login-id-type.php 2 weeks ago class-login-logout-menu.php 2 weeks ago class-maintenance-mode.php 2 weeks ago class-manage-ads-appads-txt.php 2 weeks ago class-manage-robots-txt.php 2 weeks ago class-media-files-visibility-control.php 2 weeks ago class-media-replacement.php 2 weeks ago class-multiple-user-roles.php 2 weeks ago class-navigation-menu-duplicator.php 2 weeks ago class-obfuscate-author-slugs.php 2 weeks ago class-open-external-links-in-new-tab.php 2 weeks ago class-password-protection.php 2 weeks ago class-redirect-after-login.php 2 weeks ago class-redirect-after-logout.php 2 weeks ago class-redirect-fourofour.php 2 weeks ago class-registration-date-column.php 2 weeks ago class-revisions-control.php 2 weeks ago class-search-engines-visibility.php 2 weeks ago class-settings-fields-render.php 2 weeks ago class-settings-sanitization.php 2 weeks ago class-settings-sections-fields.php 2 weeks ago class-show-custom-taxonomy-filters.php 2 weeks ago class-site-identity-on-login-page.php 2 weeks ago class-svg-upload.php 2 weeks ago class-third-party-compat.php 2 weeks ago class-various-admin-ui-enhancements.php 2 weeks ago class-view-admin-as-role.php 2 weeks ago class-wider-admin-menu.php 2 weeks ago class-wp-config-transformer.php 2 weeks ago
class-limit-login-attempts.php
647 lines
1 <?php
2
3 namespace ASENHA\Classes;
4
5 use WP_Error;
6
7 /**
8 * Class for Limit Login Attempts module
9 *
10 * @since 6.9.5
11 */
12 class Limit_Login_Attempts {
13
14 /**
15 * Maximum length for varchar columns written to the failed logins table.
16 *
17 * @since 9.0.2
18 */
19 const FAILED_LOGINS_VARCHAR_MAX = 255;
20
21 /**
22 * Ensure failed logins log table schema is up to date.
23 *
24 * @since 9.0.2
25 */
26 public function maybe_upgrade_failed_logins_log_table() {
27 $activation = new Activation();
28 $activation->maybe_upgrade_failed_logins_log_table();
29 }
30
31 /**
32 * Truncate a string to fit failed logins table varchar columns.
33 *
34 * @since 9.0.2
35 * @param string $value Value to truncate.
36 * @return string
37 */
38 private function truncate_failed_login_db_string( $value ) {
39 $value = (string) $value;
40
41 if ( function_exists( 'mb_substr' ) ) {
42 return mb_substr( $value, 0, self::FAILED_LOGINS_VARCHAR_MAX );
43 }
44
45 return substr( $value, 0, self::FAILED_LOGINS_VARCHAR_MAX );
46 }
47
48 /**
49 * Log failed logins table write errors when debugging is enabled.
50 *
51 * @since 9.0.2
52 * @param string $operation Database operation that failed.
53 */
54 private function maybe_log_failed_login_db_error( $operation ) {
55 global $wpdb;
56
57 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! empty( $wpdb->last_error ) ) {
58 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
59 error_log( 'ASE Limit Login Attempts: ' . $operation . ' failed. ' . $wpdb->last_error );
60 }
61 }
62
63 /**
64 * Maybe allow login if not locked out. Should return WP_Error object if not allowed to login.
65 *
66 * @since 2.5.0
67 */
68 public function maybe_allow_login( $user_or_error, $username, $password ) {
69 global $wpdb, $asenha_limit_login;
70
71 $table_name = $wpdb->prefix . 'asenha_failed_logins';
72
73 $this->maybe_upgrade_failed_logins_log_table();
74
75 // Maybe create table if it does not exist yet, e.g. upgraded from previous version of plugin, so, no activation methods are fired
76 $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) );
77
78 if ( $wpdb->get_var( $query ) === $table_name ) {
79 // Table already exists, do nothing.
80 } else {
81 $activation = new Activation();
82 $activation->create_failed_logins_log_table( false );
83 }
84
85 // Get values from options needed to do various checks
86 $options = get_option( ASENHA_SLUG_U, array() );
87 $login_fails_allowed = $options['login_fails_allowed'];
88 $login_lockout_maxcount = $options['login_lockout_maxcount'];
89
90 $ip_address_whitelist_raw = ( isset( $options['limit_login_attempts_ip_whitelist'] ) ) ? explode( PHP_EOL, $options['limit_login_attempts_ip_whitelist'] ) : array();
91 $ip_address_whitelist = array();
92 if ( ! empty( $ip_address_whitelist_raw ) ) {
93 foreach( $ip_address_whitelist_raw as $ip_address ) {
94 $ip_address_whitelist[] = trim( $ip_address );
95 }
96 }
97
98 $change_login_url = $options['change_login_url'];
99 $custom_login_slug = $options['custom_login_slug'];
100
101 // Instantiate object to access common methods
102 $common_methods = new Common_Methods;
103
104 // Get user/visitor IP address
105 $ip_address = $common_methods->get_user_ip_address( 'ip', 'limit-login-attempts' );
106
107 if ( ! in_array( $ip_address, $ip_address_whitelist ) ) { // IP is not whitelisted
108 // Check if IP address has failed login attempts recorded in the DB log
109 $sql = $wpdb->prepare("SELECT * FROM `" . $table_name . "` Where `ip_address` = %s", $ip_address);
110 $result = $wpdb->get_results( $sql, ARRAY_A );
111
112 $result_count = count( $result );
113
114 if ( $result_count > 0 ) { // IP address has been recorded in the database.
115 $fail_count = $result[0]['fail_count'];
116 $lockout_count = $result[0]['lockout_count'];
117 $last_fail_on = $result[0]['unixtime'];
118 } else {
119 $fail_count = 0;
120 $lockout_count = 0;
121 $last_fail_on = '';
122 }
123 } else { // IP is whitelisted
124 $result = array();
125 $result_count = 0;
126 $fail_count = 0;
127 $lockout_count = 0;
128 $last_fail_on = '';
129 }
130
131 // Initialize the global variable
132 $asenha_limit_login = array (
133 'ip_address' => $ip_address,
134 'request_uri' => sanitize_text_field( $_SERVER['REQUEST_URI'] ),
135 'ip_address_log' => $result,
136 'fail_count' => $fail_count,
137 'lockout_count' => $lockout_count,
138 'maybe_lockout' => false,
139 'extended_lockout' => false,
140 'within_lockout_period' => false,
141 'lockout_period' => 0,
142 'lockout_period_remaining' => 0,
143 'login_fails_allowed' => $login_fails_allowed,
144 'login_lockout_maxcount' => $login_lockout_maxcount,
145 // 'default_lockout_period' => 15, // 15 seconds. FOR TESTING.
146 // 'default_lockout_period' => 60, // 1 minutes in seconds
147 'default_lockout_period' => 60*15, // 15 minutes in seconds
148 // 'extended_lockout_period' => 3*60, // 3 minutes in seconds
149 'extended_lockout_period' => 24*60*60, // 24 hours in seconds
150 'change_login_url' => $change_login_url, // is custom login URL enabled?
151 'custom_login_slug' => $custom_login_slug,
152 );
153
154 if ( ! in_array( $ip_address, $ip_address_whitelist ) ) { // IP is not whitelisted
155
156 if ( $result_count > 0 ) { // IP address has been recorded in the database.
157
158 // Failed attempts have been recorded and fulfills lockout condition
159 if ( ! empty( $fail_count ) && ( ( $fail_count ) % $login_fails_allowed == 0 ) ) {
160
161 $asenha_limit_login['maybe_lockout'] = true;
162
163 // Has reached max / gone beyond number of lockouts allowed?
164 if ( $lockout_count >= $login_lockout_maxcount ) {
165 $asenha_limit_login['extended_lockout'] = true;
166 $lockout_period = $asenha_limit_login['extended_lockout_period'];
167 } else {
168 $asenha_limit_login['extended_lockout'] = false;
169 $lockout_period = $asenha_limit_login['default_lockout_period'];
170 }
171
172 $asenha_limit_login['lockout_period'] = $lockout_period;
173
174 // User/visitor is still within the lockout period
175 if ( ( time() - $last_fail_on ) <= $asenha_limit_login['lockout_period'] ) {
176
177 $asenha_limit_login['within_lockout_period'] = true;
178 $asenha_limit_login['lockout_period_remaining'] = $asenha_limit_login['lockout_period'] - ( time() - $last_fail_on );
179
180 if ( $asenha_limit_login['lockout_period_remaining'] <= 60 ) {
181
182 // Get remaining lockout period in minutes and seconds
183 $lockout_period_remaining = $asenha_limit_login['lockout_period_remaining'] . ' seconds';
184
185 } elseif ( $asenha_limit_login['lockout_period_remaining'] <= 60*60 ) {
186
187 // Get remaining lockout period in minutes and seconds
188 $lockout_period_remaining = $common_methods->seconds_to_period( $asenha_limit_login['lockout_period_remaining'], 'to-minutes-seconds' );
189
190 } elseif ( $asenha_limit_login['lockout_period_remaining'] > 60*60 && $asenha_limit_login['lockout_period_remaining'] <= 24*60*60 ) {
191
192 // Get remaining lockout period in minutes and seconds
193 $lockout_period_remaining = $common_methods->seconds_to_period( $asenha_limit_login['lockout_period_remaining'], 'to-hours-minutes-seconds' );
194
195 } elseif ( $asenha_limit_login['lockout_period_remaining'] > 24*60*60 ) {
196
197 // Get remaining lockout period in minutes and seconds
198 $lockout_period_remaining = $common_methods->seconds_to_period( $asenha_limit_login['lockout_period_remaining'], 'to-days-hours-minutes-seconds' );
199
200 }
201
202 $error = new WP_Error( 'ip_address_blocked', '<b>WARNING:</b> You\'ve been locked out. You can login again in ' . $lockout_period_remaining . '.' );
203
204 return $error;
205
206 } else { // User/visitor is no longer within the lockout period
207
208 $asenha_limit_login['within_lockout_period'] = false;
209
210 if ( $lockout_count == $login_lockout_maxcount ) {
211
212 // Remove the DB log entry for the current IP address. i.e. release from extended lockout
213
214 $where = array( 'ip_address' => $ip_address );
215 $where_format = array( '%s' );
216
217 // Delete existing data in the database
218 $wpdb->delete(
219 $table_name,
220 $where,
221 $where_format
222 );
223
224 }
225
226 return $user_or_error;
227
228 }
229
230 } else {
231
232 $asenha_limit_login['maybe_lockout'] = false;
233
234 return $user_or_error;
235
236 }
237
238 } else { // IP address has not been recorded in the database.
239
240 return $user_or_error;
241
242 }
243
244 } else { // IP is whitelisted
245 return $user_or_error;
246 }
247 }
248
249 /**
250 * Handle login errors
251 *
252 * @link https://developer.wordpress.org/reference/classes/wp_error/#methods
253 * @since 2.5.0
254 */
255 public function login_error_handler( $errors, $redirect_to ) {
256 global $asenha_limit_login;
257
258 if ( is_wp_error( $errors ) ) {
259
260 $error_codes = $errors->get_error_codes();
261
262 foreach ( $error_codes as $error_code ) {
263
264 if ( $error_code == 'invalid_username' || $error_code == 'incorrect_password' ) {
265
266 // Remove default error messages that may give out valueable info to hackers
267
268 $errors->remove( 'invalid_username' ); // Outputs info that says username does not exist. May encourage login attempt with a different username instead.
269
270 $errors->remove( 'incorrect_password' ); // Outputs info that implies username exist. May encourage login attempt with a different password.
271
272 // Add a new error message that does not provide useful clues to hackers
273 $errors->add( 'invalid_username_or_incorrect_password', '<b>' . __( 'Error:', 'admin-site-enhancements' ) . '</b> ' . __( 'Invalid username/email or incorrect password.', 'admin-site-enhancements' ) );
274
275 // $errors->add( 'another_error_code', 'The error message.' );
276
277 }
278
279 }
280
281 }
282
283 return $errors;
284 }
285
286 /**
287 * Disable login form inputs via CSS
288 *
289 * @since 2.5.0
290 */
291 public function maybe_hide_login_form() {
292 global $asenha_limit_login;
293
294 if ( isset( $asenha_limit_login['within_lockout_period'] ) && $asenha_limit_login['within_lockout_period'] ) {
295
296 // Hide logo, login form and the links below it
297 ?>
298 <script>
299 document.addEventListener("DOMContentLoaded", function(event) {
300 var loginForm = document.getElementById("loginform");
301 loginForm.remove();
302 });
303 </script>
304 <style type="text/css">
305
306 body.login {
307 background:#f6d6d7;
308 }
309
310 #login h1,
311 #loginform,
312 #login #nav,
313 #backtoblog,
314 .language-switcher {
315 display: none;
316 }
317
318 @media screen and (max-height: 550px) {
319
320 #login {
321 padding: 80px 0 20px !important;
322 }
323
324 }
325
326 </style>
327 <?php
328 } else {
329 $options = get_option( ASENHA_SLUG_U, array() );
330 $login_fails_allowed = $options['login_fails_allowed'];
331 $page_was_reloaded = isset( $_GET['rl'] ) && 1 == sanitize_text_field( $_GET['rl'] ) ? true : false;
332
333 if ( isset( $asenha_limit_login['fail_count'] )
334 && ( ( $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
335 || ( 2 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
336 || ( 3 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
337 || ( 4 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
338 || ( 5 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
339 || ( 6 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
340 )
341 ) {
342 if ( array_key_exists( 'change_login_url', $options ) && $options['change_login_url'] ) {
343 // Custom Login URL is enabled, e.g. /manage
344 // Do nothing
345 } else {
346 // Default login URL, i.e. /wp-login.php
347 // Reload the login page so we get the up-to-date data in $asenha_limit_login
348 // Only reload if page was not reloaded before. This prevents infinite reloads.
349 if ( ! $page_was_reloaded ) {
350 ?>
351 <script>
352 let url = window.location.href;
353 if (url.indexOf('?') > -1){
354 url += '&rl=1'
355 } else {
356 url += '?rl=1'
357 }
358 location.replace(url);
359 </script>
360 <?php
361 }
362
363 }
364 }
365 }
366 }
367
368 /**
369 * Add login error message on top of the login form
370 *
371 * @since 2.5.0
372 */
373 public function add_failed_login_message( $message ) {
374 global $asenha_limit_login;
375
376 if ( isset( $_REQUEST['failed_login'] ) && $_REQUEST['failed_login'] == 'true' ) {
377
378 if ( ! is_null( $asenha_limit_login ) && isset( $asenha_limit_login['within_lockout_period'] ) && ! $asenha_limit_login['within_lockout_period'] ) {
379
380 $message = '<div id="login_error" class="notice notice-error"><b>' . __( 'Error:', 'admin-site-enhancements' ) . '</b> ' . __( 'Invalid username/email or incorrect password.', 'admin-site-enhancements' ) . '</div>';
381
382 }
383
384 }
385
386 return $message;
387 }
388
389 /**
390 * Log failed login attempts
391 *
392 * @since 2.5.0
393 */
394 public function log_failed_login( $username ) {
395 global $wpdb, $asenha_limit_login;
396
397 $table_name = $wpdb->prefix . 'asenha_failed_logins';
398
399 $this->maybe_upgrade_failed_logins_log_table();
400
401 $ip_address = isset( $asenha_limit_login['ip_address'] ) ? $asenha_limit_login['ip_address'] : '';
402 $request_uri = isset( $asenha_limit_login['request_uri'] ) ? $asenha_limit_login['request_uri'] : '';
403 $login_fails_allowed = isset( $asenha_limit_login['login_fails_allowed'] ) ? $asenha_limit_login['login_fails_allowed'] : 3;
404 $login_lockout_maxcount = isset( $asenha_limit_login['login_lockout_maxcount'] ) ? $asenha_limit_login['login_lockout_maxcount'] : 3;
405
406 $username = $this->truncate_failed_login_db_string( $username );
407 $request_uri = $this->truncate_failed_login_db_string( $request_uri );
408
409 // Check if the IP address has been used in a failed login attempt before, i.e. has it been recorded in the database?
410 $sql = $wpdb->prepare( "SELECT * FROM `" . $table_name . "` WHERE `ip_address` = %s", $ip_address );
411 $result = $wpdb->get_results( $sql, ARRAY_A );
412 if ( $result ) {
413 $result_count = count( $result );
414 } else {
415 $result_count = 0;
416 }
417
418 // Update logged info for the IP address in the global variable
419 if ( $result ) {
420 $asenha_limit_login['ip_address_log'] = $result;
421 }
422
423 if ( $result_count == 0 ) { // IP address has not been recorded in the database.
424
425 $new_fail_count = 1;
426 $new_lockout_count = 0;
427
428 } else { // IP address has been recorded in the database.
429
430 $new_fail_count = $result[0]['fail_count'] + 1;
431 $new_lockout_count = floor( ( $result[0]['fail_count'] + 1 ) / $login_fails_allowed );
432
433 }
434
435 // Get the URL where login failed, i.e. where brute force attack might be happening
436 // $login_url = ( ! empty( $_SERVER['HTTPS'] ) ? 'https://' : 'http://') . sanitize_text_field( $_SERVER['HTTP_HOST'] ) . sanitize_text_field( $_SERVER['REQUEST_URI'] );
437
438 // Time stamps
439 $unixtime = time();
440 if ( function_exists( 'wp_date' ) ) {
441 $datetime_wp = wp_date( 'Y-m-d H:i:s', $unixtime );
442 } else {
443 $datetime_wp = date_i18n( 'Y-m-d H:i:s', $unixtime );
444 }
445
446 $data = array(
447 'ip_address' => $ip_address,
448 'username' => $username,
449 'fail_count' => $new_fail_count,
450 'lockout_count' => $new_lockout_count,
451 'request_uri' => $request_uri,
452 'unixtime' => $unixtime,
453 'datetime_wp' => $datetime_wp,
454 'info' => '',
455 );
456
457 $data_format = array(
458 '%s', // string
459 '%s', // string
460 '%d', // integer
461 '%d', // integer
462 '%s', // string
463 '%d', // integer
464 '%s', // string
465 '%s', // string
466 );
467
468 if ( $result_count == 0 ) {
469
470 // Insert into the database
471 $db_result = $wpdb->insert(
472 $table_name,
473 $data,
474 $data_format
475 );
476
477 if ( false === $db_result ) {
478 $this->maybe_log_failed_login_db_error( 'insert' );
479 }
480
481 } else {
482
483 $fail_count = $result[0]['fail_count'];
484 $lockout_count = $result[0]['lockout_count'];
485 $last_fail_on = $result[0]['unixtime'];
486
487 $where = array( 'ip_address' => $ip_address );
488 $where_format = array( '%s' );
489
490 // Failed attempts have been recorded and fulfills lockout condition
491 if ( ! empty( $fail_count )
492 && ( $login_fails_allowed > 0 )
493 && ( $fail_count % $login_fails_allowed == 0 )
494 ) {
495
496 // Has reached max / gone beyond number of lockouts allowed?
497 if ( $lockout_count >= $login_lockout_maxcount ) {
498 $asenha_limit_login['extended_lockout'] = true;
499 $lockout_period = $asenha_limit_login['extended_lockout_period'];
500 } else {
501 $asenha_limit_login['extended_lockout'] = false;
502 $lockout_period = $asenha_limit_login['default_lockout_period'];
503 }
504
505 $asenha_limit_login['lockout_period'] = $lockout_period;
506
507 // User/visitor is still within the lockout period
508 if ( ( time() - $last_fail_on ) <= $lockout_period ) {
509
510 // Do nothing
511
512 } else {
513
514 if ( $lockout_count < $login_lockout_maxcount ) {
515
516 // Update existing data in the database
517 $db_result = $wpdb->update(
518 $table_name,
519 $data,
520 $where,
521 $data_format,
522 $where_format
523 );
524
525 if ( false === $db_result ) {
526 $this->maybe_log_failed_login_db_error( 'update' );
527 }
528
529 }
530
531 }
532
533 } else {
534
535 // Update existing data in the database
536 $db_result = $wpdb->update(
537 $table_name,
538 $data,
539 $where,
540 $data_format,
541 $where_format
542 );
543
544 if ( false === $db_result ) {
545 $this->maybe_log_failed_login_db_error( 'update' );
546 }
547
548 }
549
550 }
551 }
552
553 /**
554 * Clear failed login attempts log after successful login
555 *
556 * @since 2.5.0
557 */
558 public function clear_failed_login_log() {
559 global $wpdb, $asenha_limit_login;
560
561 $table_name = $wpdb->prefix . 'asenha_failed_logins';
562 $ip_address = isset( $asenha_limit_login['ip_address'] ) ? $asenha_limit_login['ip_address'] : '';
563
564 // Remove the DB log entry for the current IP address.
565
566 $where = array( 'ip_address' => $ip_address );
567 $where_format = array( '%s' );
568
569 $wpdb->delete(
570 $table_name,
571 $where,
572 $where_format
573 );
574 }
575
576 /**
577 * Trigger scheduling of failed login attempts log clean up event.
578 *
579 * @since 7.1.1
580 */
581 public function trigger_clear_or_schedule_log_clean_up_by_amount( $option_name ) {
582 if ( ASENHA_SLUG_U === $option_name ) {
583 $this->clear_or_schedule_log_clean_up_by_amount();
584 }
585 }
586
587 /**
588 * Schedule failed login attempts log clean up event
589 *
590 * @link https://plugins.trac.wordpress.org/browser/lana-email-logger/tags/1.1.0/lana-email-logger.php#L750
591 * @since 7.8.3
592 */
593 public function clear_or_schedule_log_clean_up_by_amount() {
594 $options = get_option( ASENHA_SLUG_U, array() );
595 $limit_login_attempts = isset( $options['limit_login_attempts'] ) ? $options['limit_login_attempts'] : false;
596 $failed_login_attempts_log_schedule_cleanup_by_amount = isset( $options['failed_login_attempts_log_schedule_cleanup_by_amount'] ) ? $options['failed_login_attempts_log_schedule_cleanup_by_amount'] : false;
597
598 // If module or scheduled clean up is not enabled, clear the schedule.
599 if ( ! $limit_login_attempts || ! $failed_login_attempts_log_schedule_cleanup_by_amount ) {
600 wp_clear_scheduled_hook( 'asenha_failed_login_attempts_log_cleanup_by_amount' );
601 return;
602 }
603
604 // If there's no next scheduled clean up event, let's schedule one
605 if ( ! wp_next_scheduled( 'asenha_failed_login_attempts_log_cleanup_by_amount' ) ) {
606 wp_schedule_event( time(), 'hourly', 'asenha_failed_login_attempts_log_cleanup_by_amount' );
607 }
608 }
609
610 /**
611 * Perform clean up of failed login attempts log by the amount of entries to keep
612 *
613 * @link https://plugins.trac.wordpress.org/browser/lana-email-logger/tags/1.1.0/lana-email-logger.php#L768
614 * @since 7.8.3
615 */
616 public function perform_failed_login_attempts_log_clean_up_by_amount() {
617 global $wpdb;
618
619 $options = get_option( ASENHA_SLUG_U, array() );
620 $limit_login_attempts = isset( $options['limit_login_attempts'] ) ? $options['limit_login_attempts'] : false;
621 $failed_login_attempts_log_schedule_cleanup_by_amount = isset( $options['failed_login_attempts_log_schedule_cleanup_by_amount'] ) ? $options['failed_login_attempts_log_schedule_cleanup_by_amount'] : false;
622 $failed_login_attempts_log_entries_amount_to_keep = 1000;
623
624 // Bail and clear any orphan schedule if clean up should not run.
625 if ( ! $limit_login_attempts || ! $failed_login_attempts_log_schedule_cleanup_by_amount ) {
626 wp_clear_scheduled_hook( 'asenha_failed_login_attempts_log_cleanup_by_amount' );
627 return;
628 }
629
630 $table_name = $wpdb->prefix . 'asenha_failed_logins';
631
632 // Maybe create table if it does not exist yet, e.g. module enabled but no login attempt yet.
633 $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) );
634
635 if ( $wpdb->get_var( $query ) === $table_name ) {
636 // Table already exists, do nothing.
637 } else {
638 $activation = new Activation();
639 $activation->create_failed_logins_log_table( false );
640 }
641
642 $wpdb->query( "DELETE failed_login_entries FROM " . $table_name . "
643 AS failed_login_entries JOIN ( SELECT id FROM " . $table_name . " ORDER BY id DESC LIMIT 1 OFFSET " . $failed_login_attempts_log_entries_amount_to_keep . " )
644 AS failed_login_entries_limit ON failed_login_entries.id <= failed_login_entries_limit.id;" );
645 }
646
647 }