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 +266 -132 5.3.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,23 +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 -
32 - public const LOCKOUT_UA = 'ua_lockout';
33 - // Different IP Lockout types.
41 + // Group of 'custom' cases.
34 42 public const LOCKOUT_IP_CUSTOM = 'custom_lockout';
35 43
36 - public const INFINITE_SCROLL_SIZE = 50;
37 -
38 44 /**
39 45 * Table name.
40 46 *
41 47 * @var string
@@ -106,8 +112,34 @@
106 112 */
107 113 public $country_iso_code;
108 114
109 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 + /**
110 142 * Query the logs based on the provided filters and pagination settings.
111 143 *
112 144 * @param array $filters An array of filters to apply to the query. Default is an empty array.
113 145 * The following filters are supported:
@@ -141,34 +173,28 @@
141 173 $filters['to'],
142 174 )
143 175 );
144 176
145 - if ( isset( $filters['ip'] ) && ! empty( $filters['ip'] ) ) {
177 + if ( isset( $filters['ip'] ) && '' !== $filters['ip'] ) {
146 178 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
147 179 }
148 - 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'] ) {
149 184 $orm->where( 'type', $filters['type'] );
150 185 }
151 186
152 - if ( ! empty( $filters['ban_status'] ) ) {
153 - $ban_status_where = self::ban_status_where( $filters['ban_status'] );
154 -
155 - if ( 3 === count( $ban_status_where ) ) {
156 - $orm->where( ...$ban_status_where );
157 - }
187 + if ( isset( $filters['ban_status'] ) && '' !== $filters['ban_status'] ) {
188 + self::apply_ban_status_filter( $orm, $filters );
158 189 }
159 190
160 - if ( ! empty( $order_by ) && ! empty( $order ) ) {
191 + if ( '' !== $order_by && '' !== $order ) {
161 192 $orm->order_by( $order_by, $order );
162 193 }
163 -
164 - if ( - 1 === (int) $page_size ) {
165 - $page_size = self::INFINITE_SCROLL_SIZE;
166 - }
167 -
168 - if ( false !== $page_size ) {
194 + if ( $page_size > 0 ) {
169 195 $offset = ( $paged - 1 ) * $page_size;
170 - $orm->limit( "$offset,$page_size" );
196 + $orm->limit( $page_size, $offset );
171 197 }
172 198
173 199 return $orm->get();
174 200 }
@@ -183,9 +209,9 @@
183 209 * @param array $filters An array of additional filters to apply to the query. Default is an empty array.
184 210 * The following filters are supported:
185 211 * - ban_status: The ban status to filter by.
186 212 *
187 - * @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.
188 214 */
189 215 public static function count( $date_from, $date_to, $type, $ip = '', $filters = array() ): ?string {
190 216 $orm = self::get_orm();
191 217 $orm->get_repository( self::class )
@@ -197,26 +223,24 @@
197 223 $date_to,
198 224 )
199 225 );
200 226
201 - if ( ! empty( $type ) ) {
202 - if ( is_array( $type ) ) {
203 - $orm->where( 'type', 'in', $type );
204 - } else {
205 - $orm->where( 'type', $type );
206 - }
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 );
207 231 }
208 232
209 - if ( ! empty( $ip ) ) {
233 + if ( is_string( $ip ) && '' !== trim( $ip ) ) {
210 234 $orm->where( 'ip', 'like', "%$ip%" );
211 235 }
212 236
213 - if ( ! empty( $filters['ban_status'] ) ) {
214 - $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 + }
215 240
216 - if ( 3 === count( $ban_status_where ) ) {
217 - $orm->where( ...$ban_status_where );
218 - }
241 + if ( isset( $filters['ban_status'] ) && '' !== trim( $filters['ban_status'] ) ) {
242 + self::apply_ban_status_filter( $orm, array_merge( $filters, array( 'type' => $type ) ) );
219 243 }
220 244
221 245 return $orm->count();
222 246 }
@@ -221,8 +245,31 @@
221 245 return $orm->count();
222 246 }
223 247
224 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 + /**
225 272 * Count login lockout in the last 7 days.
226 273 *
227 274 * @return string|null
228 275 */
@@ -241,13 +288,13 @@
241 288 public static function count_404_lockout_last_7_days(): ?string {
242 289 $start = strtotime( '-7 days' );
243 290 $end = time();
244 291
245 - return self::count( $start, $end, self::LOCKOUT_404 );
292 + return self::count( $start, $end, self::get_404_lockout_types() );
246 293 }
247 294
248 295 /**
249 - * Count UA lockout in the last 7 days.
296 + * Count all UA lockouts in the last 7 days.
250 297 *
251 298 * @return string|null
252 299 */
253 300 public static function count_ua_lockout_last_7_days(): ?string {
@@ -253,16 +300,28 @@
253 300 public static function count_ua_lockout_last_7_days(): ?string {
254 301 $start = strtotime( '-7 days' );
255 302 $end = time();
256 303
257 - return self::count( $start, $end, self::LOCKOUT_UA );
304 + return self::count( $start, $end, self::get_ua_lockout_types() );
258 305 }
259 306
260 307 /**
261 - * A shortcut for quickly count lockout in last 24 hours.
308 + * Count all UA lockouts in the last 24 hours.
262 309 *
263 310 * @return string|null
264 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 + */
265 324 public static function count_lockout_in_24_hours(): ?string {
266 325 $start = strtotime( '-24 hours' );
267 326 $end = time();
268 327
@@ -268,13 +327,9 @@
268 327
269 328 return self::count(
270 329 $start,
271 330 $end,
272 - array(
273 - self::AUTH_LOCK,
274 - self::LOCKOUT_404,
275 - self::LOCKOUT_UA,
276 - )
331 + self::get_all_lockout_types()
277 332 );
278 333 }
279 334
280 335 /**
@@ -288,13 +343,9 @@
288 343
289 344 return self::count(
290 345 $start,
291 346 $end,
292 - array(
293 - self::AUTH_LOCK,
294 - self::LOCKOUT_404,
295 - self::LOCKOUT_UA,
296 - )
347 + self::get_all_lockout_types()
297 348 );
298 349 }
299 350
300 351 /**
@@ -308,14 +359,10 @@
308 359
309 360 return self::count(
310 361 $start,
311 362 $end,
312 - array(
313 - self::AUTH_LOCK,
314 - self::LOCKOUT_404,
315 - self::LOCKOUT_UA,
316 - // LOCKOUT_IP_CUSTOM is not taken into account.
317 - )
363 + // LOCKOUT_IP_CUSTOM is not taken into account.
364 + self::get_all_lockout_types()
318 365 );
319 366 }
320 367
321 368 /**
@@ -358,8 +405,37 @@
358 405 ->truncate();
359 406 }
360 407
361 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 + /**
362 438 * Remove logs based on timestamp and limit.
363 439 *
364 440 * @param int $timestamp The timestamp to filter logs by.
365 441 * @param int $limit The maximum number of logs to delete.
@@ -386,8 +462,11 @@
386 462 $today_midnight = strtotime( '-24 hours', $current_time );
387 463 $first_this_week = strtotime( '-7 days', $current_time );
388 464 $first_this_month = strtotime( '-30 days', $current_time );
389 465
466 + $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'";
467 + $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'";
468 +
390 469 // Prepare columns.
391 470 $select = array(
392 471 'MAX(date) as lockout_last',
393 472 'COUNT(*) as lockout_this_month',
@@ -392,19 +471,19 @@
392 471 'MAX(date) as lockout_last',
393 472 'COUNT(*) as lockout_this_month',
394 473 // 24 hours
395 474 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
396 - "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",
397 476 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
398 - "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",
399 478 // 7 days
400 - "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",
401 480 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
402 - "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",
403 482 // 30 days
404 - "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",
405 484 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
406 - "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",
407 486 );
408 487 $select = implode( ',', $select );
409 488
410 489 $orm = self::get_orm();
@@ -410,9 +489,9 @@
410 489 $orm = self::get_orm();
411 490 $result = $orm->get_repository( self::class )
412 491 ->select( $select )
413 492 // LOCKOUT_IP_CUSTOM is not taken into account.
414 - ->where( 'type', 'in', array( self::LOCKOUT_404, self::AUTH_LOCK, self::LOCKOUT_UA ) )
493 + ->where( 'type', 'in', self::get_all_lockout_types() )
415 494 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
416 495 ->get_results();
417 496
418 497 return $result[0] ?? array();
@@ -420,9 +499,9 @@
420 499
421 500 /**
422 501 * Returns the log tag based on the given type.
423 502 *
424 - * @param int $type The type of the log.
503 + * @param string $type The type of the log.
425 504 *
426 505 * @return string The log tag.
427 506 */
428 507 protected static function get_log_tag( $type ): string {
@@ -427,10 +506,16 @@
427 506 */
428 507 protected static function get_log_tag( $type ): string {
429 508 switch ( $type ) {
430 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:
431 513 case self::ERROR_404:
432 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:
433 518 $tag = '404';
434 519 break;
435 520 case self::AUTH_FAIL:
436 521 case self::AUTH_LOCK:
@@ -442,8 +527,10 @@
442 527 case self::IP_UNLOCK:
443 528 $tag = 'Unlock';
444 529 break;
445 530 case self::LOCKOUT_UA:
531 + case self::LOCKOUT_MALICIOUS_BOT:
532 + case self::LOCKOUT_FAKE_BOT:
446 533 default:
447 534 $tag = 'bots';
448 535 break;
449 536 }
@@ -450,9 +537,8 @@
450 537
451 538 return $tag;
452 539 }
453 540
454 -
455 541 /**
456 542 * Returns the CSS class for the log container based on the given type.
457 543 *
458 544 * @param string $type The type of the log.
@@ -462,14 +548,22 @@
462 548 protected static function get_log_container_class( $type ): string {
463 549 switch ( $type ) {
464 550 case self::AUTH_LOCK:
465 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:
466 555 case self::LOCKOUT_UA:
556 + case self::LOCKOUT_MALICIOUS_BOT:
557 + case self::LOCKOUT_FAKE_BOT:
467 558 $class = 'sui-error';
468 559 break;
469 560 case self::AUTH_FAIL:
470 561 case self::ERROR_404:
471 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:
472 566 default:
473 567 $class = 'sui-warning';
474 568 break;
475 569 }
@@ -522,8 +616,27 @@
522 616 ->first();
523 617 }
524 618
525 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 + /**
526 639 * Delete current log.
527 640 */
528 641 public function delete() {
529 642 $orm = self::get_orm();
@@ -534,8 +647,51 @@
534 647 );
535 648 }
536 649
537 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 + /**
538 694 * Prepare user-agent where condition based on the ban status variant.
539 695 *
540 696 * @param string $ban_status_type Ban status type.
541 697 *
@@ -542,30 +698,31 @@
542 698 * @return array Where condition arguments or empty array.
543 699 */
544 700 private static function ban_status_where( $ban_status_type ): array {
545 701 $table_lockout = wd_di()->get( Table_Lockout::class );
702 + $ua_model = wd_di()->get( User_Agent_Lockout::class );
546 703
547 704 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
548 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
549 -
550 - $blocklist = $ua_model->get_lockout_list( 'blocklist' );
551 - $allowlist = $ua_model->get_lockout_list( 'allowlist' );
552 -
553 - $all = array_merge( $blocklist, $allowlist );
554 -
555 - 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 ) );
556 711 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
557 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
558 -
559 - $blocklist = $ua_model->get_lockout_list( 'blocklist' );
560 -
561 - 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 ) );
562 718 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
563 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
564 -
565 719 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
566 -
567 - 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 ) );
568 725 }
569 726
570 727 return array();
571 728 }
@@ -578,21 +735,15 @@
578 735 * @return array
579 736 * @since 3.11.0
580 737 */
581 738 public static function format_logs( array $logs ): array {
582 - $data = array();
583 - $ua_model = wd_di()->get( User_Agent_Lockout::class );
739 + $data = array();
584 740 foreach ( $logs as $item ) {
585 - $ip_model = Lockout_Ip::get( $item->ip );
586 -
587 741 // Escape object properties received from end user.
588 742 $item->log = sanitize_textarea_field( $item->log );
589 743 $item->tried = sanitize_textarea_field( $item->tried );
744 + $log = $item->export();
590 745
591 - $arr_ip_statuses = $ip_model->get_access_status();
592 -
593 - $log = $item->export();
594 -
595 746 // Escape array keys received from end user.
596 747 $log['log'] = sanitize_textarea_field( $log['log'] );
597 748 $log['tried'] = sanitize_textarea_field( $log['tried'] );
598 749
@@ -599,42 +750,12 @@
599 750 $log['date'] = $item->format_date_time( $item->date );
600 751 $log['format_date'] = $item->get_date( $item->date );
601 752 $log['tag'] = self::get_log_tag( $item->type );
602 753 $log['container_class'] = self::get_log_container_class( $item->type );
603 - if ( self::LOCKOUT_UA === $item->type ) {
604 - if ( User_Agent::REASON_BAD_POST === $item->tried ) {
605 - $log['description'] = esc_html__(
606 - '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.',
607 - 'defender-security'
608 - );
609 - $log['type_label'] = esc_html__( 'Type', 'defender-security' );
610 - $log['type_value'] = esc_html__( 'Empty Headers', 'defender-security' );
611 - $arr_statuses = $arr_ip_statuses;
612 - } else {
613 - $log['description'] = sprintf(
614 - /* translators: 1. Log. 2. User agent. */
615 - esc_html__(
616 - '%1$s: %2$s. This user agent is considered bad bots and may harm your site.',
617 - 'defender-security'
618 - ),
619 - sanitize_textarea_field( $item->log ),
620 - '<strong>' . sanitize_textarea_field( $item->user_agent ) . '</strong>'
621 - );
622 - $log['type_label'] = esc_html__( 'User Agent name', 'defender-security' );
623 - $log['type_value'] = sanitize_textarea_field( $item->user_agent );
624 - $log['access_status_ip'] = $arr_ip_statuses;
625 - $arr_statuses = $ua_model->get_access_status( $item->user_agent );
626 - }
627 - } else {
754 + if ( self::LOCKOUT_UA !== $item->type ) {
628 755 $log['description'] = sanitize_textarea_field( $item->log );
629 - $log['type_label'] = esc_html__( 'Type', 'defender-security' );
630 - $log['type_value'] = str_replace( '_', ' ', $item->type );
631 - $arr_statuses = $arr_ip_statuses;
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();
@@ -652,17 +773,30 @@
652 773
653 774 $orm = self::get_orm();
654 775 // Query the latest log for the current IP.
655 776 $latest_log = $orm->get_repository( self::class )
656 - ->select( 'date' )
657 - ->where( 'ip', $this->ip )
658 - ->order_by( 'date', 'desc' )
659 - ->first();
777 + ->select( 'date' )
778 + ->where( 'ip', $this->ip )
779 + ->order_by( 'date', 'desc' )
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 }