PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/model/class-lockout-log.php +144 -84 5.7.1 → 6.3.0 View file →
@@ -9,10 +9,12 @@
9 9
10 10 use WP_Defender\DB;
11 11 use Calotes\Base\Model;
12 12 use WP_Defender\Traits\Formats;
13 +use WP_Defender\Traits\Country;
13 14 use WP_Defender\Component\User_Agent;
14 15 use WP_Defender\Component\Table_Lockout;
16 +use WP_Defender\Model\Setting\Blacklist_Lockout;
15 17 use WP_Defender\Model\Setting\User_Agent_Lockout;
16 18
17 19 /**
18 20 * Model for the lockout log table.
@@ -19,8 +21,9 @@
19 21 */
20 22 class Lockout_Log extends DB {
21 23
22 24 use Formats;
25 + use Country;
23 26
24 27 // The Firewall log hierarchy. Parent items: 'login', '404', 'bots', 'unlock' and 'custom'.
25 28 // Group of 'login' cases.
26 29 public const AUTH_FAIL = 'auth_fail', AUTH_LOCK = 'auth_lock';
@@ -37,10 +40,8 @@
37 40 public const IP_UNLOCK = 'ip_unlock';
38 41 // Group of 'custom' cases.
39 42 public const LOCKOUT_IP_CUSTOM = 'custom_lockout';
40 43
41 - public const INFINITE_SCROLL_SIZE = 50;
42 -
43 44 /**
44 45 * Table name.
45 46 *
46 47 * @var string
@@ -111,8 +112,34 @@
111 112 */
112 113 public $country_iso_code;
113 114
114 115 /**
116 + * Factory method to instantiate and populate a Lockout_Log model with request data.
117 + *
118 + * @param string $ip The IP address.
119 + * @param string|null $tried The attempted URI, username, or user agent.
120 + *
121 + * @return self
122 + */
123 + public static function create( string $ip, ?string $tried = null ): self {
124 + $model = new self();
125 + $model->ip = $ip;
126 + $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
127 + $model->user_agent = '' !== $user_agent ? User_Agent::fast_cleaning( $user_agent ) : '';
128 + $model->date = time();
129 + $model->tried = $tried ?? $model->user_agent;
130 + $model->blog_id = get_current_blog_id();
131 +
132 + $ip_to_country = $model->ip_to_country( $ip );
133 +
134 + if ( isset( $ip_to_country['iso'] ) ) {
135 + $model->country_iso_code = $ip_to_country['iso'];
136 + }
137 +
138 + return $model;
139 + }
140 +
141 + /**
115 142 * Query the logs based on the provided filters and pagination settings.
116 143 *
117 144 * @param array $filters An array of filters to apply to the query. Default is an empty array.
118 145 * The following filters are supported:
@@ -149,8 +176,11 @@
149 176
150 177 if ( isset( $filters['ip'] ) && '' !== $filters['ip'] ) {
151 178 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
152 179 }
180 + if ( isset( $filters['user_agent'] ) && '' !== $filters['user_agent'] ) {
181 + $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
182 + }
153 183 if ( isset( $filters['type'] ) && '' !== $filters['type'] ) {
154 184 $orm->where( 'type', $filters['type'] );
155 185 }
156 186
@@ -203,14 +233,14 @@
203 233 if ( is_string( $ip ) && '' !== trim( $ip ) ) {
204 234 $orm->where( 'ip', 'like', "%$ip%" );
205 235 }
206 236
237 + if ( isset( $filters['user_agent'] ) && '' !== trim( $filters['user_agent'] ) ) {
238 + $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
239 + }
240 +
207 241 if ( isset( $filters['ban_status'] ) && '' !== trim( $filters['ban_status'] ) ) {
208 - $ban_status_where = self::ban_status_where( $filters['ban_status'] );
209 -
210 - if ( 3 === count( $ban_status_where ) ) {
211 - $orm->where( ...$ban_status_where );
212 - }
242 + self::apply_ban_status_filter( $orm, array_merge( $filters, array( 'type' => $type ) ) );
213 243 }
214 244
215 245 return $orm->count();
216 246 }
@@ -230,13 +260,13 @@
230 260 /**
231 261 * Get all lockout types.
232 262 */
233 263 public static function get_all_lockout_types(): array {
234 - $arr = self::get_404_lockout_types();
235 - $arr[] = self::AUTH_LOCK;
236 - $arr[] = self::LOCKOUT_UA;
237 -
238 - return $arr;
264 + return array_merge(
265 + self::get_404_lockout_types(),
266 + array( self::AUTH_LOCK ),
267 + self::get_ua_lockout_types()
268 + );
239 269 }
240 270
241 271 /**
242 272 * Count login lockout in the last 7 days.
@@ -262,9 +292,9 @@
262 292 return self::count( $start, $end, self::get_404_lockout_types() );
263 293 }
264 294
265 295 /**
266 - * Count UA lockout in the last 7 days.
296 + * Count all UA lockouts in the last 7 days.
267 297 *
268 298 * @return string|null
269 299 */
270 300 public static function count_ua_lockout_last_7_days(): ?string {
@@ -270,16 +300,28 @@
270 300 public static function count_ua_lockout_last_7_days(): ?string {
271 301 $start = strtotime( '-7 days' );
272 302 $end = time();
273 303
274 - return self::count( $start, $end, self::LOCKOUT_UA );
304 + return self::count( $start, $end, self::get_ua_lockout_types() );
275 305 }
276 306
277 307 /**
278 - * A shortcut for quickly count lockout in last 24 hours.
308 + * Count all UA lockouts in the last 24 hours.
279 309 *
280 310 * @return string|null
281 311 */
312 + public static function count_ua_lockouts_in_24_hours(): ?string {
313 + $start = strtotime( '-24 hours' );
314 + $end = time();
315 +
316 + return self::count( $start, $end, self::get_ua_lockout_types() );
317 + }
318 +
319 + /**
320 + * A shortcut for quickly count ALL lockouts in last 24 hours.
321 + *
322 + * @return string|null
323 + */
282 324 public static function count_lockout_in_24_hours(): ?string {
283 325 $start = strtotime( '-24 hours' );
284 326 $end = time();
285 327
@@ -363,8 +405,37 @@
363 405 ->truncate();
364 406 }
365 407
366 408 /**
409 + * Get log types that are attempts/errors (not lockout events).
410 + *
411 + * @return array
412 + */
413 + public static function get_attempt_types(): array {
414 + return array(
415 + self::AUTH_FAIL,
416 + self::ERROR_404,
417 + self::ERROR_404_IGNORE,
418 + self::ERROR_404_XSS,
419 + self::ERROR_404_NON_EXISTENT_PLUGIN,
420 + self::ERROR_404_NON_EXISTENT_THEME,
421 + );
422 + }
423 +
424 + /**
425 + * Delete only lockout-type log records, preserving attempt/error entries.
426 + *
427 + * @return int|false Number of rows deleted or false on failure.
428 + */
429 + public static function delete_lockout_records() {
430 + $orm = self::get_orm();
431 +
432 + return $orm->get_repository( self::class )
433 + ->where( 'type', 'not in', self::get_attempt_types() )
434 + ->delete_all();
435 + }
436 +
437 + /**
367 438 * Remove logs based on timestamp and limit.
368 439 *
369 440 * @param int $timestamp The timestamp to filter logs by.
370 441 * @param int $limit The maximum number of logs to delete.
@@ -391,8 +462,11 @@
391 462 $today_midnight = strtotime( '-24 hours', $current_time );
392 463 $first_this_week = strtotime( '-7 days', $current_time );
393 464 $first_this_month = strtotime( '-30 days', $current_time );
394 465
466 + $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'";
467 + $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'";
468 +
395 469 // Prepare columns.
396 470 $select = array(
397 471 'MAX(date) as lockout_last',
398 472 'COUNT(*) as lockout_this_month',
@@ -397,19 +471,19 @@
397 471 'MAX(date) as lockout_last',
398 472 'COUNT(*) as lockout_this_month',
399 473 // 24 hours
400 474 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
401 - "COUNT(IF(date > {$today_midnight} AND type IN ('" . self::LOCKOUT_404 . "', '" . self::LOCKOUT_404_XSS . "', '" . self::LOCKOUT_404_NON_EXISTENT_THEME . "', '" . self::LOCKOUT_404_NON_EXISTENT_PLUGIN . "'), 1, NULL)) as lockout_404_today",
475 + "COUNT(IF(date > {$today_midnight} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_today",
402 476 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
403 - "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_today",
477 + "COUNT(IF(date > {$today_midnight} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_today",
404 478 // 7 days
405 - "COUNT(IF(date > {$first_this_week} AND type IN ('" . self::LOCKOUT_404 . "', '" . self::LOCKOUT_404_XSS . "', '" . self::LOCKOUT_404_NON_EXISTENT_THEME . "', '" . self::LOCKOUT_404_NON_EXISTENT_PLUGIN . "'), 1, NULL)) as lockout_404_this_week",
479 + "COUNT(IF(date > {$first_this_week} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_week",
406 480 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
407 - "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_week",
481 + "COUNT(IF(date > {$first_this_week} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_week",
408 482 // 30 days
409 - "COUNT(IF(date > {$first_this_month} AND type IN ('" . self::LOCKOUT_404 . "', '" . self::LOCKOUT_404_XSS . "', '" . self::LOCKOUT_404_NON_EXISTENT_THEME . "', '" . self::LOCKOUT_404_NON_EXISTENT_PLUGIN . "'), 1, NULL)) as lockout_404_this_month",
483 + "COUNT(IF(date > {$first_this_month} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_month",
410 484 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
411 - "COUNT(IF(date > {$first_this_month} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_month",
485 + "COUNT(IF(date > {$first_this_month} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_month",
412 486 );
413 487 $select = implode( ',', $select );
414 488
415 489 $orm = self::get_orm();
@@ -542,8 +616,27 @@
542 616 ->first();
543 617 }
544 618
545 619 /**
620 + * Get logs by an array of IDs.
621 + *
622 + * @param int[] $ids Array of log IDs.
623 + *
624 + * @return array
625 + */
626 + public static function find_by_ids( array $ids ): array {
627 + if ( array() === $ids ) {
628 + return array();
629 + }
630 +
631 + $orm = self::get_orm();
632 +
633 + return $orm->get_repository( self::class )
634 + ->where( 'id', 'in', $ids )
635 + ->get();
636 + }
637 +
638 + /**
546 639 * Delete current log.
547 640 */
548 641 public function delete() {
549 642 $orm = self::get_orm();
@@ -561,26 +654,40 @@
561 654 * @param array $filters The filters array.
562 655 */
563 656 private static function apply_ban_status_filter( $orm, $filters ) {
564 657 $ban_status = $filters['ban_status'];
565 - $type = $filters['type'] ?? 'all';
658 + $type = $filters['type'] ?? '';
659 + $ua_types = self::get_ua_lockout_types();
566 660
567 - // Define lockout type categories.
568 - $ua_types = array( self::get_ua_lockout_types() );
661 + $where = self::ban_status_where( $ban_status );
569 662
570 - if ( 'all' === $type || '' === $type ) {
571 - // For 'all' type, only show UA types with ban_status filtering.
572 - $ban_status_where = self::ban_status_where( $ban_status );
573 - if ( 3 === count( $ban_status_where ) ) {
663 + if ( 3 === count( $where ) ) {
664 + if ( in_array( $type, $ua_types, true ) ) {
665 + $orm->where( ...$where );
666 + } elseif ( '' === $type ) {
574 667 $orm->where( 'type', 'in', $ua_types );
575 - $orm->where( ...$ban_status_where );
668 + $orm->where( ...$where );
576 669 }
577 - } elseif ( in_array( $type, $ua_types, true ) ) {
578 - // For UA-specific types, apply UA filtering.
579 - $ban_status_where = self::ban_status_where( $ban_status );
580 - if ( 3 === count( $ban_status_where ) ) {
581 - $orm->where( ...$ban_status_where );
670 + return;
671 + }
672 +
673 + $table_lockout = wd_di()->get( Table_Lockout::class );
674 + $bl_model = wd_di()->get( Blacklist_Lockout::class );
675 +
676 + if ( $table_lockout::STATUS_BAN === $ban_status ) {
677 + $list = $bl_model->get_list( 'blocklist' );
678 + // Empty blocklist means no IPs are banned — force zero results.
679 + $orm->where( 'ip', 'in', array() === $list ? array( '' ) : $list );
680 + } elseif ( $table_lockout::STATUS_NOT_BAN === $ban_status ) {
681 + $list = $bl_model->get_list( 'blocklist' );
682 + // Empty blocklist means all IPs are "not banned" — no filter needed.
683 + if ( array() !== $list ) {
684 + $orm->where( 'ip', 'not in', $list );
582 685 }
686 + } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status ) {
687 + $list = $bl_model->get_list( 'allowlist' );
688 + // Empty allowlist means no IPs are allowlisted — force zero results.
689 + $orm->where( 'ip', 'in', array() === $list ? array( '' ) : $list );
583 690 }
584 691 }
585 692
586 693 /**
@@ -628,21 +735,15 @@
628 735 * @return array
629 736 * @since 3.11.0
630 737 */
631 738 public static function format_logs( array $logs ): array {
632 - $data = array();
633 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
739 + $data = array();
634 740 foreach ( $logs as $item ) {
635 - $ip_model = Lockout_Ip::get( $item->ip );
636 -
637 741 // Escape object properties received from end user.
638 742 $item->log = sanitize_textarea_field( $item->log );
639 743 $item->tried = sanitize_textarea_field( $item->tried );
744 + $log = $item->export();
640 745
641 - $arr_ip_statuses = $ip_model->get_access_status();
642 -
643 - $log = $item->export();
644 -
645 746 // Escape array keys received from end user.
646 747 $log['log'] = sanitize_textarea_field( $log['log'] );
647 748 $log['tried'] = sanitize_textarea_field( $log['tried'] );
648 749
@@ -649,51 +750,10 @@
649 750 $log['date'] = $item->format_date_time( $item->date );
650 751 $log['format_date'] = $item->get_date( $item->date );
651 752 $log['tag'] = self::get_log_tag( $item->type );
652 753 $log['container_class'] = self::get_log_container_class( $item->type );
653 - if ( self::LOCKOUT_UA === $item->type ) {
654 - if ( User_Agent::REASON_BAD_POST === $item->tried ) {
655 - $log['description'] = esc_html__(
656 - 'Lockout occurred due to attempted access with empty User-Agent and Referer headers. By default, IP addresses that send POST requests with empty User-Agent and Referer headers will be automatically banned. You can disable this option in the User Agent Banning settings, or you can unban the locked out IP address below.',
657 - 'defender-security'
658 - );
659 - $log['type_label'] = esc_html__( 'Type', 'defender-security' );
660 - $log['type_value'] = esc_html__( 'Empty Headers', 'defender-security' );
661 - $arr_statuses = $arr_ip_statuses;
662 - } else {
663 - $log['description'] = sprintf(
664 - /* translators: 1. Log. 2. User agent. */
665 - esc_html__(
666 - '%1$s: %2$s. This user agent is considered bad bots and may harm your site.',
667 - 'defender-security'
668 - ),
669 - sanitize_textarea_field( $item->log ),
670 - '<strong>' . sanitize_textarea_field( $item->user_agent ) . '</strong>'
671 - );
672 - $log['type_label'] = esc_html__( 'User Agent name', 'defender-security' );
673 - $log['type_value'] = sanitize_textarea_field( $item->user_agent );
674 - $log['access_status_ip'] = $arr_ip_statuses;
675 - $arr_statuses = $ua_model->get_access_status( $item->user_agent );
676 - }
677 - } else {
754 + if ( self::LOCKOUT_UA !== $item->type ) {
678 755 $log['description'] = sanitize_textarea_field( $item->log );
679 - $log['type_label'] = esc_html__( 'Type', 'defender-security' );
680 - $log['type_value'] = str_replace( '_', ' ', $item->type );
681 - $arr_statuses = $arr_ip_statuses;
682 -
683 - if ( 'malicious_bot' === $item->type ) {
684 - $log['access_status_ua'] = $ua_model->get_access_status( $item->user_agent );
685 - }
686 - }
687 - // There may be several statuses.
688 - $log['access_status'] = $arr_statuses;
689 -
690 - // For UA lockout types, show UA status; for others show IP status.
691 - if ( in_array( $item->type, array( self::get_ua_lockout_types() ), true ) ) {
692 - $ua_statuses = $ua_model->get_access_status( $item->user_agent );
693 - $log['access_status_text'] = $ip_model->get_access_status_text( $ua_statuses[0] ?? 'na' );
694 - } else {
695 - $log['access_status_text'] = $ip_model->get_access_status_text( $arr_statuses[0] );
696 756 }
697 757 $data[] = $log;
698 758 }
699 759