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 +262 -128 5.4.0 → 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,24 +21,27 @@
19 21 */
20 22 class Lockout_Log extends DB {
21 23
22 24 use Formats;
25 + use Country;
23 26
24 - public const AUTH_FAIL = 'auth_fail';
25 - public const AUTH_LOCK = 'auth_lock';
27 + // The Firewall log hierarchy. Parent items: 'login', '404', 'bots', 'unlock' and 'custom'.
28 + // Group of 'login' cases.
29 + public const AUTH_FAIL = 'auth_fail', AUTH_LOCK = 'auth_lock';
30 + // Group of '404' cases.
31 + public const ERROR_404 = '404_error', LOCKOUT_404 = '404_lockout', ERROR_404_IGNORE = '404_error_ignore';
32 + // New log cases since v5.7.0.
33 + public const ERROR_404_XSS = 'xss_attempt', LOCKOUT_404_XSS = 'xss_lockout';
34 + public const ERROR_404_NON_EXISTENT_PLUGIN = 'plugin_attempt', LOCKOUT_404_NON_EXISTENT_PLUGIN = 'plugin_lockout';
35 + public const ERROR_404_NON_EXISTENT_THEME = 'theme_attempt', LOCKOUT_404_NON_EXISTENT_THEME = 'theme_lockout';
36 + // Group of 'bots' cases.
37 + // 'ua_lockout' includes 'bad_user_agent' due to attempted login with banned user agent && 'bad_post' due to empty User-Agent and Referer headers).
38 + public const LOCKOUT_MALICIOUS_BOT = 'malicious_bot', LOCKOUT_FAKE_BOT = 'fake_bot', LOCKOUT_UA = 'ua_lockout';
39 + // Group of 'unlock' cases.
26 40 public const IP_UNLOCK = 'ip_unlock';
27 -
28 - public const ERROR_404 = '404_error';
29 - public const LOCKOUT_404 = '404_lockout';
30 - public const ERROR_404_IGNORE = '404_error_ignore';
31 - public const LOCKOUT_BOT_TRAP = 'bot_trap';
32 -
33 - public const LOCKOUT_UA = 'ua_lockout';
34 - // Different IP Lockout types.
41 + // Group of 'custom' cases.
35 42 public const LOCKOUT_IP_CUSTOM = 'custom_lockout';
36 43
37 - public const INFINITE_SCROLL_SIZE = 50;
38 -
39 44 /**
40 45 * Table name.
41 46 *
42 47 * @var string
@@ -107,8 +112,34 @@
107 112 */
108 113 public $country_iso_code;
109 114
110 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 + /**
111 142 * Query the logs based on the provided filters and pagination settings.
112 143 *
113 144 * @param array $filters An array of filters to apply to the query. Default is an empty array.
114 145 * The following filters are supported:
@@ -142,29 +173,28 @@
142 173 $filters['to'],
143 174 )
144 175 );
145 176
146 - if ( isset( $filters['ip'] ) && ! empty( $filters['ip'] ) ) {
177 + if ( isset( $filters['ip'] ) && '' !== $filters['ip'] ) {
147 178 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
148 179 }
149 - if ( isset( $filters['type'] ) && ! empty( $filters['type'] ) ) {
180 + if ( isset( $filters['user_agent'] ) && '' !== $filters['user_agent'] ) {
181 + $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
182 + }
183 + if ( isset( $filters['type'] ) && '' !== $filters['type'] ) {
150 184 $orm->where( 'type', $filters['type'] );
151 185 }
152 186
153 - if ( ! empty( $filters['ban_status'] ) ) {
154 - $ban_status_where = self::ban_status_where( $filters['ban_status'] );
155 -
156 - if ( 3 === count( $ban_status_where ) ) {
157 - $orm->where( ...$ban_status_where );
158 - }
187 + if ( isset( $filters['ban_status'] ) && '' !== $filters['ban_status'] ) {
188 + self::apply_ban_status_filter( $orm, $filters );
159 189 }
160 190
161 - if ( ! empty( $order_by ) && ! empty( $order ) ) {
191 + if ( '' !== $order_by && '' !== $order ) {
162 192 $orm->order_by( $order_by, $order );
163 193 }
164 - if ( false !== $page_size && -1 !== (int) $page_size ) {
194 + if ( $page_size > 0 ) {
165 195 $offset = ( $paged - 1 ) * $page_size;
166 - $orm->limit( "$offset,$page_size" );
196 + $orm->limit( $page_size, $offset );
167 197 }
168 198
169 199 return $orm->get();
170 200 }
@@ -179,9 +209,9 @@
179 209 * @param array $filters An array of additional filters to apply to the query. Default is an empty array.
180 210 * The following filters are supported:
181 211 * - ban_status: The ban status to filter by.
182 212 *
183 - * @return int|null The number of records matching the provided filters, or null if an error occurred.
213 + * @return string|null The number of records matching the provided filters, or null if an error occurred.
184 214 */
185 215 public static function count( $date_from, $date_to, $type, $ip = '', $filters = array() ): ?string {
186 216 $orm = self::get_orm();
187 217 $orm->get_repository( self::class )
@@ -193,26 +223,24 @@
193 223 $date_to,
194 224 )
195 225 );
196 226
197 - if ( ! empty( $type ) ) {
198 - if ( is_array( $type ) ) {
199 - $orm->where( 'type', 'in', $type );
200 - } else {
201 - $orm->where( 'type', $type );
202 - }
227 + if ( is_array( $type ) && array() !== $type ) {
228 + $orm->where( 'type', 'in', $type );
229 + } elseif ( is_string( $type ) && '' !== trim( $type ) ) {
230 + $orm->where( 'type', $type );
203 231 }
204 232
205 - if ( ! empty( $ip ) ) {
233 + if ( is_string( $ip ) && '' !== trim( $ip ) ) {
206 234 $orm->where( 'ip', 'like', "%$ip%" );
207 235 }
208 236
209 - if ( ! empty( $filters['ban_status'] ) ) {
210 - $ban_status_where = self::ban_status_where( $filters['ban_status'] );
237 + if ( isset( $filters['user_agent'] ) && '' !== trim( $filters['user_agent'] ) ) {
238 + $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
239 + }
211 240
212 - if ( 3 === count( $ban_status_where ) ) {
213 - $orm->where( ...$ban_status_where );
214 - }
241 + if ( isset( $filters['ban_status'] ) && '' !== trim( $filters['ban_status'] ) ) {
242 + self::apply_ban_status_filter( $orm, array_merge( $filters, array( 'type' => $type ) ) );
215 243 }
216 244
217 245 return $orm->count();
218 246 }
@@ -217,8 +245,31 @@
217 245 return $orm->count();
218 246 }
219 247
220 248 /**
249 + * Get 404 lockout types.
250 + */
251 + public static function get_404_lockout_types(): array {
252 + return array(
253 + self::LOCKOUT_404,
254 + self::LOCKOUT_404_XSS,
255 + self::LOCKOUT_404_NON_EXISTENT_PLUGIN,
256 + self::LOCKOUT_404_NON_EXISTENT_THEME,
257 + );
258 + }
259 +
260 + /**
261 + * Get all lockout types.
262 + */
263 + public static function get_all_lockout_types(): array {
264 + return array_merge(
265 + self::get_404_lockout_types(),
266 + array( self::AUTH_LOCK ),
267 + self::get_ua_lockout_types()
268 + );
269 + }
270 +
271 + /**
221 272 * Count login lockout in the last 7 days.
222 273 *
223 274 * @return string|null
224 275 */
@@ -237,13 +288,13 @@
237 288 public static function count_404_lockout_last_7_days(): ?string {
238 289 $start = strtotime( '-7 days' );
239 290 $end = time();
240 291
241 - return self::count( $start, $end, self::LOCKOUT_404 );
292 + return self::count( $start, $end, self::get_404_lockout_types() );
242 293 }
243 294
244 295 /**
245 - * Count UA lockout in the last 7 days.
296 + * Count all UA lockouts in the last 7 days.
246 297 *
247 298 * @return string|null
248 299 */
249 300 public static function count_ua_lockout_last_7_days(): ?string {
@@ -249,16 +300,28 @@
249 300 public static function count_ua_lockout_last_7_days(): ?string {
250 301 $start = strtotime( '-7 days' );
251 302 $end = time();
252 303
253 - return self::count( $start, $end, self::LOCKOUT_UA );
304 + return self::count( $start, $end, self::get_ua_lockout_types() );
254 305 }
255 306
256 307 /**
257 - * A shortcut for quickly count lockout in last 24 hours.
308 + * Count all UA lockouts in the last 24 hours.
258 309 *
259 310 * @return string|null
260 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 + */
261 324 public static function count_lockout_in_24_hours(): ?string {
262 325 $start = strtotime( '-24 hours' );
263 326 $end = time();
264 327
@@ -264,13 +327,9 @@
264 327
265 328 return self::count(
266 329 $start,
267 330 $end,
268 - array(
269 - self::AUTH_LOCK,
270 - self::LOCKOUT_404,
271 - self::LOCKOUT_UA,
272 - )
331 + self::get_all_lockout_types()
273 332 );
274 333 }
275 334
276 335 /**
@@ -284,13 +343,9 @@
284 343
285 344 return self::count(
286 345 $start,
287 346 $end,
288 - array(
289 - self::AUTH_LOCK,
290 - self::LOCKOUT_404,
291 - self::LOCKOUT_UA,
292 - )
347 + self::get_all_lockout_types()
293 348 );
294 349 }
295 350
296 351 /**
@@ -304,14 +359,10 @@
304 359
305 360 return self::count(
306 361 $start,
307 362 $end,
308 - array(
309 - self::AUTH_LOCK,
310 - self::LOCKOUT_404,
311 - self::LOCKOUT_UA,
312 - // LOCKOUT_IP_CUSTOM is not taken into account.
313 - )
363 + // LOCKOUT_IP_CUSTOM is not taken into account.
364 + self::get_all_lockout_types()
314 365 );
315 366 }
316 367
317 368 /**
@@ -354,8 +405,37 @@
354 405 ->truncate();
355 406 }
356 407
357 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 + /**
358 438 * Remove logs based on timestamp and limit.
359 439 *
360 440 * @param int $timestamp The timestamp to filter logs by.
361 441 * @param int $limit The maximum number of logs to delete.
@@ -382,8 +462,11 @@
382 462 $today_midnight = strtotime( '-24 hours', $current_time );
383 463 $first_this_week = strtotime( '-7 days', $current_time );
384 464 $first_this_month = strtotime( '-30 days', $current_time );
385 465
466 + $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'";
467 + $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'";
468 +
386 469 // Prepare columns.
387 470 $select = array(
388 471 'MAX(date) as lockout_last',
389 472 'COUNT(*) as lockout_this_month',
@@ -388,19 +471,19 @@
388 471 'MAX(date) as lockout_last',
389 472 'COUNT(*) as lockout_this_month',
390 473 // 24 hours
391 474 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
392 - "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_404 . "', 1, NULL)) as lockout_404_today",
475 + "COUNT(IF(date > {$today_midnight} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_today",
393 476 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
394 - "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",
395 478 // 7 days
396 - "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_404 . "', 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",
397 480 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
398 - "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",
399 482 // 30 days
400 - "COUNT(IF(date > {$first_this_month} AND type = '" . self::LOCKOUT_404 . "', 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",
401 484 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
402 - "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",
403 486 );
404 487 $select = implode( ',', $select );
405 488
406 489 $orm = self::get_orm();
@@ -406,9 +489,9 @@
406 489 $orm = self::get_orm();
407 490 $result = $orm->get_repository( self::class )
408 491 ->select( $select )
409 492 // LOCKOUT_IP_CUSTOM is not taken into account.
410 - ->where( 'type', 'in', array( self::LOCKOUT_404, self::AUTH_LOCK, self::LOCKOUT_UA ) )
493 + ->where( 'type', 'in', self::get_all_lockout_types() )
411 494 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
412 495 ->get_results();
413 496
414 497 return $result[0] ?? array();
@@ -416,9 +499,9 @@
416 499
417 500 /**
418 501 * Returns the log tag based on the given type.
419 502 *
420 - * @param int $type The type of the log.
503 + * @param string $type The type of the log.
421 504 *
422 505 * @return string The log tag.
423 506 */
424 507 protected static function get_log_tag( $type ): string {
@@ -423,10 +506,16 @@
423 506 */
424 507 protected static function get_log_tag( $type ): string {
425 508 switch ( $type ) {
426 509 case self::LOCKOUT_404:
510 + case self::LOCKOUT_404_XSS:
511 + case self::LOCKOUT_404_NON_EXISTENT_THEME:
512 + case self::LOCKOUT_404_NON_EXISTENT_PLUGIN:
427 513 case self::ERROR_404:
428 514 case self::ERROR_404_IGNORE:
515 + case self::ERROR_404_XSS:
516 + case self::ERROR_404_NON_EXISTENT_THEME:
517 + case self::ERROR_404_NON_EXISTENT_PLUGIN:
429 518 $tag = '404';
430 519 break;
431 520 case self::AUTH_FAIL:
432 521 case self::AUTH_LOCK:
@@ -438,8 +527,10 @@
438 527 case self::IP_UNLOCK:
439 528 $tag = 'Unlock';
440 529 break;
441 530 case self::LOCKOUT_UA:
531 + case self::LOCKOUT_MALICIOUS_BOT:
532 + case self::LOCKOUT_FAKE_BOT:
442 533 default:
443 534 $tag = 'bots';
444 535 break;
445 536 }
@@ -457,15 +548,22 @@
457 548 protected static function get_log_container_class( $type ): string {
458 549 switch ( $type ) {
459 550 case self::AUTH_LOCK:
460 551 case self::LOCKOUT_404:
552 + case self::LOCKOUT_404_XSS:
553 + case self::LOCKOUT_404_NON_EXISTENT_THEME:
554 + case self::LOCKOUT_404_NON_EXISTENT_PLUGIN:
461 555 case self::LOCKOUT_UA:
462 - case self::LOCKOUT_BOT_TRAP:
556 + case self::LOCKOUT_MALICIOUS_BOT:
557 + case self::LOCKOUT_FAKE_BOT:
463 558 $class = 'sui-error';
464 559 break;
465 560 case self::AUTH_FAIL:
466 561 case self::ERROR_404:
467 562 case self::ERROR_404_IGNORE:
563 + case self::ERROR_404_XSS:
564 + case self::ERROR_404_NON_EXISTENT_THEME:
565 + case self::ERROR_404_NON_EXISTENT_PLUGIN:
468 566 default:
469 567 $class = 'sui-warning';
470 568 break;
471 569 }
@@ -518,8 +616,27 @@
518 616 ->first();
519 617 }
520 618
521 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 + /**
522 639 * Delete current log.
523 640 */
524 641 public function delete() {
525 642 $orm = self::get_orm();
@@ -530,8 +647,51 @@
530 647 );
531 648 }
532 649
533 650 /**
651 + * Apply ban status filtering based on lockout type.
652 + *
653 + * @param object $orm The ORM query builder.
654 + * @param array $filters The filters array.
655 + */
656 + private static function apply_ban_status_filter( $orm, $filters ) {
657 + $ban_status = $filters['ban_status'];
658 + $type = $filters['type'] ?? '';
659 + $ua_types = self::get_ua_lockout_types();
660 +
661 + $where = self::ban_status_where( $ban_status );
662 +
663 + if ( 3 === count( $where ) ) {
664 + if ( in_array( $type, $ua_types, true ) ) {
665 + $orm->where( ...$where );
666 + } elseif ( '' === $type ) {
667 + $orm->where( 'type', 'in', $ua_types );
668 + $orm->where( ...$where );
669 + }
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 );
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 );
690 + }
691 + }
692 +
693 + /**
534 694 * Prepare user-agent where condition based on the ban status variant.
535 695 *
536 696 * @param string $ban_status_type Ban status type.
537 697 *
@@ -538,30 +698,31 @@
538 698 * @return array Where condition arguments or empty array.
539 699 */
540 700 private static function ban_status_where( $ban_status_type ): array {
541 701 $table_lockout = wd_di()->get( Table_Lockout::class );
702 + $ua_model = wd_di()->get( User_Agent_Lockout::class );
542 703
543 704 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
544 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
545 -
546 - $blocklist = $ua_model->get_lockout_list( 'blocklist' );
547 - $allowlist = $ua_model->get_lockout_list( 'allowlist' );
548 -
549 - $all = array_merge( $blocklist, $allowlist );
550 -
551 - return array( 'user_agent', 'not regexp', implode( '|', $all ) );
705 + $blocklist = $ua_model->get_all_selected_blocklist_ua();
706 + if ( array() === $blocklist ) {
707 + return array();
708 + }
709 + $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
710 + return array( 'user_agent', 'not regexp', '(?i)' . implode( '|', $escaped ) );
552 711 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
553 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
554 -
555 - $blocklist = $ua_model->get_lockout_list( 'blocklist' );
556 -
557 - return array( 'user_agent', 'regexp', implode( '|', $blocklist ) );
712 + $blocklist = $ua_model->get_all_selected_blocklist_ua();
713 + if ( array() === $blocklist ) {
714 + return array();
715 + }
716 + $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
717 + return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
558 718 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
559 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
560 -
561 719 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
562 -
563 - return array( 'user_agent', 'regexp', implode( '|', $allowlist ) );
720 + if ( array() === $allowlist ) {
721 + return array();
722 + }
723 + $escaped = array_map( 'preg_quote', $allowlist, array_fill( 0, count( $allowlist ), '' ) );
724 + return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
564 725 }
565 726
566 727 return array();
567 728 }
@@ -574,21 +735,15 @@
574 735 * @return array
575 736 * @since 3.11.0
576 737 */
577 738 public static function format_logs( array $logs ): array {
578 - $data = array();
579 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
739 + $data = array();
580 740 foreach ( $logs as $item ) {
581 - $ip_model = Lockout_Ip::get( $item->ip );
582 -
583 741 // Escape object properties received from end user.
584 742 $item->log = sanitize_textarea_field( $item->log );
585 743 $item->tried = sanitize_textarea_field( $item->tried );
744 + $log = $item->export();
586 745
587 - $arr_ip_statuses = $ip_model->get_access_status();
588 -
589 - $log = $item->export();
590 -
591 746 // Escape array keys received from end user.
592 747 $log['log'] = sanitize_textarea_field( $log['log'] );
593 748 $log['tried'] = sanitize_textarea_field( $log['tried'] );
594 749
@@ -595,46 +750,12 @@
595 750 $log['date'] = $item->format_date_time( $item->date );
596 751 $log['format_date'] = $item->get_date( $item->date );
597 752 $log['tag'] = self::get_log_tag( $item->type );
598 753 $log['container_class'] = self::get_log_container_class( $item->type );
599 - if ( self::LOCKOUT_UA === $item->type ) {
600 - if ( User_Agent::REASON_BAD_POST === $item->tried ) {
601 - $log['description'] = esc_html__(
602 - '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.',
603 - 'defender-security'
604 - );
605 - $log['type_label'] = esc_html__( 'Type', 'defender-security' );
606 - $log['type_value'] = esc_html__( 'Empty Headers', 'defender-security' );
607 - $arr_statuses = $arr_ip_statuses;
608 - } else {
609 - $log['description'] = sprintf(
610 - /* translators: 1. Log. 2. User agent. */
611 - esc_html__(
612 - '%1$s: %2$s. This user agent is considered bad bots and may harm your site.',
613 - 'defender-security'
614 - ),
615 - sanitize_textarea_field( $item->log ),
616 - '<strong>' . sanitize_textarea_field( $item->user_agent ) . '</strong>'
617 - );
618 - $log['type_label'] = esc_html__( 'User Agent name', 'defender-security' );
619 - $log['type_value'] = sanitize_textarea_field( $item->user_agent );
620 - $log['access_status_ip'] = $arr_ip_statuses;
621 - $arr_statuses = $ua_model->get_access_status( $item->user_agent );
622 - }
623 - } else {
754 + if ( self::LOCKOUT_UA !== $item->type ) {
624 755 $log['description'] = sanitize_textarea_field( $item->log );
625 - $log['type_label'] = esc_html__( 'Type', 'defender-security' );
626 - $log['type_value'] = str_replace( '_', ' ', $item->type );
627 - $arr_statuses = $arr_ip_statuses;
628 -
629 - if ( 'bot_trap' === $item->type ) {
630 - $log['access_status_ua'] = $ua_model->get_access_status( $item->user_agent );
631 - }
632 756 }
633 - // There may be several statuses.
634 - $log['access_status'] = $arr_statuses;
635 - $log['access_status_text'] = $ip_model->get_access_status_text( $arr_statuses[0] );
636 - $data[] = $log;
757 + $data[] = $log;
637 758 }
638 759
639 760 return $data;
640 761 }
@@ -645,9 +766,9 @@
645 766 * @return bool True if the IP should be added to the database, false otherwise.
646 767 */
647 768 public function has_recent_ip_log(): bool {
648 769 // Ensure IP is set before proceeding.
649 - if ( empty( $this->ip ) ) {
770 + if ( ! is_string( $this->ip ) || '' === trim( $this->ip ) ) {
650 771 return false;
651 772 }
652 773
653 774 $orm = self::get_orm();
@@ -657,12 +778,25 @@
657 778 ->where( 'ip', $this->ip )
658 779 ->order_by( 'date', 'desc' )
659 780 ->first();
660 781
661 - if ( $latest_log ) {
782 + if ( null !== $latest_log ) {
662 783 // Return true if the log is within the 5-minute timeframe.
663 784 return ( time() - $latest_log->date ) <= 300;
664 785 }
665 786
666 787 return false;
788 + }
789 +
790 + /**
791 + * Get UA lockout types.
792 + *
793 + * @return array
794 + */
795 + public static function get_ua_lockout_types(): array {
796 + return array(
797 + self::LOCKOUT_UA,
798 + self::LOCKOUT_MALICIOUS_BOT,
799 + self::LOCKOUT_FAKE_BOT,
800 + );
667 801 }
668 802 }