PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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
defender-security / src / model / class-lockout-log.php

class-lockout-log.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.0, at src/model/class-lockout-log.php

777 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles interactions with the database table for lockout logs.
4 *
5 * @package WP_Defender\Model
6 */
7
8 namespace WP_Defender\Model;
9
10 use WP_Defender\DB;
11 use Calotes\Base\Model;
12 use WP_Defender\Traits\Formats;
13 use WP_Defender\Component\User_Agent;
14 use WP_Defender\Component\Table_Lockout;
15 use WP_Defender\Model\Setting\Blacklist_Lockout;
16 use WP_Defender\Model\Setting\User_Agent_Lockout;
17
18 /**
19 * Model for the lockout log table.
20 */
21 class Lockout_Log extends DB {
22
23 use Formats;
24
25 // The Firewall log hierarchy. Parent items: 'login', '404', 'bots', 'unlock' and 'custom'.
26 // Group of 'login' cases.
27 public const AUTH_FAIL = 'auth_fail', AUTH_LOCK = 'auth_lock';
28 // Group of '404' cases.
29 public const ERROR_404 = '404_error', LOCKOUT_404 = '404_lockout', ERROR_404_IGNORE = '404_error_ignore';
30 // New log cases since v5.7.0.
31 public const ERROR_404_XSS = 'xss_attempt', LOCKOUT_404_XSS = 'xss_lockout';
32 public const ERROR_404_NON_EXISTENT_PLUGIN = 'plugin_attempt', LOCKOUT_404_NON_EXISTENT_PLUGIN = 'plugin_lockout';
33 public const ERROR_404_NON_EXISTENT_THEME = 'theme_attempt', LOCKOUT_404_NON_EXISTENT_THEME = 'theme_lockout';
34 // Group of 'bots' cases.
35 // 'ua_lockout' includes 'bad_user_agent' due to attempted login with banned user agent && 'bad_post' due to empty User-Agent and Referer headers).
36 public const LOCKOUT_MALICIOUS_BOT = 'malicious_bot', LOCKOUT_FAKE_BOT = 'fake_bot', LOCKOUT_UA = 'ua_lockout';
37 // Group of 'unlock' cases.
38 public const IP_UNLOCK = 'ip_unlock';
39 // Group of 'custom' cases.
40 public const LOCKOUT_IP_CUSTOM = 'custom_lockout';
41
42 public const INFINITE_SCROLL_SIZE = 50;
43
44 /**
45 * Table name.
46 *
47 * @var string
48 */
49 protected $table = 'defender_lockout_log';
50
51 /**
52 * Primary key column.
53 *
54 * @var int
55 * @defender_property
56 */
57 public $id;
58 /**
59 * Table column for log.
60 *
61 * @var string
62 * @defender_property
63 */
64 public $log;
65 /**
66 * Table column for IP address.
67 *
68 * @var string
69 * @defender_property
70 */
71 public $ip;
72 /**
73 * Table column for date.
74 *
75 * @var int
76 * @defender_property
77 */
78 public $date;
79 /**
80 * Table column for user agent.
81 *
82 * @var string
83 * @defender_property
84 */
85 public $user_agent;
86 /**
87 * Table column for type.
88 *
89 * @var string
90 * @defender_property
91 */
92 public $type;
93 /**
94 * Table column for blog id.
95 *
96 * @var int
97 * @defender_property
98 */
99 public $blog_id;
100 /**
101 * Table column for tried.
102 *
103 * @var string
104 * @defender_property
105 */
106 public $tried;
107 /**
108 * Table column for country iso code.
109 *
110 * @var string
111 * @defender_property
112 */
113 public $country_iso_code;
114
115 /**
116 * Query the logs based on the provided filters and pagination settings.
117 *
118 * @param array $filters An array of filters to apply to the query. Default is an empty array.
119 * The following filters are supported:
120 * - from: The start date for the date range filter.
121 * - to: The end date for the date range filter.
122 * - ip: The IP address to filter by.
123 * - type: The type of log to filter by.
124 * - ban_status: The ban status to filter by.
125 * @param int $paged The page number of the results to retrieve. Default is 1.
126 * @param string $order_by The field to order the results by. Default is 'id'.
127 * @param string $order The order direction of the results. Default is 'desc'.
128 * @param int $page_size The number of results to retrieve per page. Default is 50.
129 * Set to -1 to retrieve all results.
130 *
131 * @return array An array of Lockout_Log objects representing the queried logs.
132 */
133 public static function query_logs(
134 $filters = array(),
135 $paged = 1,
136 $order_by = 'id',
137 $order = 'desc',
138 $page_size = 50
139 ): array {
140 $orm = self::get_orm();
141 $orm->get_repository( self::class )
142 ->where(
143 'date',
144 'between',
145 array(
146 $filters['from'],
147 $filters['to'],
148 )
149 );
150
151 if ( isset( $filters['ip'] ) && '' !== $filters['ip'] ) {
152 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
153 }
154 if ( isset( $filters['user_agent'] ) && '' !== $filters['user_agent'] ) {
155 $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
156 }
157 if ( isset( $filters['type'] ) && '' !== $filters['type'] ) {
158 $orm->where( 'type', $filters['type'] );
159 }
160
161 if ( isset( $filters['ban_status'] ) && '' !== $filters['ban_status'] ) {
162 self::apply_ban_status_filter( $orm, $filters );
163 }
164
165 if ( '' !== $order_by && '' !== $order ) {
166 $orm->order_by( $order_by, $order );
167 }
168 if ( $page_size > 0 ) {
169 $offset = ( $paged - 1 ) * $page_size;
170 $orm->limit( $page_size, $offset );
171 }
172
173 return $orm->get();
174 }
175
176 /**
177 * Count the number of records in the database table based on the provided filters.
178 *
179 * @param mixed $date_from The start date for the date range filter.
180 * @param mixed $date_to The end date for the date range filter.
181 * @param mixed $type The type of log to filter by.
182 * @param string $ip The IP address to filter by. Default is an empty string.
183 * @param array $filters An array of additional filters to apply to the query. Default is an empty array.
184 * The following filters are supported:
185 * - ban_status: The ban status to filter by.
186 *
187 * @return string|null The number of records matching the provided filters, or null if an error occurred.
188 */
189 public static function count( $date_from, $date_to, $type, $ip = '', $filters = array() ): ?string {
190 $orm = self::get_orm();
191 $orm->get_repository( self::class )
192 ->where(
193 'date',
194 'between',
195 array(
196 $date_from,
197 $date_to,
198 )
199 );
200
201 if ( is_array( $type ) && array() !== $type ) {
202 $orm->where( 'type', 'in', $type );
203 } elseif ( is_string( $type ) && '' !== trim( $type ) ) {
204 $orm->where( 'type', $type );
205 }
206
207 if ( is_string( $ip ) && '' !== trim( $ip ) ) {
208 $orm->where( 'ip', 'like', "%$ip%" );
209 }
210
211 if ( isset( $filters['user_agent'] ) && '' !== trim( $filters['user_agent'] ) ) {
212 $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
213 }
214
215 if ( isset( $filters['ban_status'] ) && '' !== trim( $filters['ban_status'] ) ) {
216 self::apply_ban_status_filter( $orm, array_merge( $filters, array( 'type' => $type ) ) );
217 }
218
219 return $orm->count();
220 }
221
222 /**
223 * Get 404 lockout types.
224 */
225 public static function get_404_lockout_types(): array {
226 return array(
227 self::LOCKOUT_404,
228 self::LOCKOUT_404_XSS,
229 self::LOCKOUT_404_NON_EXISTENT_PLUGIN,
230 self::LOCKOUT_404_NON_EXISTENT_THEME,
231 );
232 }
233
234 /**
235 * Get all lockout types.
236 */
237 public static function get_all_lockout_types(): array {
238 return array_merge(
239 self::get_404_lockout_types(),
240 array( self::AUTH_LOCK ),
241 self::get_ua_lockout_types()
242 );
243 }
244
245 /**
246 * Count login lockout in the last 7 days.
247 *
248 * @return string|null
249 */
250 public static function count_login_lockout_last_7_days(): ?string {
251 $start = strtotime( '-7 days' );
252 $end = time();
253
254 return self::count( $start, $end, self::AUTH_LOCK );
255 }
256
257 /**
258 * Count 404 lockout in the last 7 days.
259 *
260 * @return string|null
261 */
262 public static function count_404_lockout_last_7_days(): ?string {
263 $start = strtotime( '-7 days' );
264 $end = time();
265
266 return self::count( $start, $end, self::get_404_lockout_types() );
267 }
268
269 /**
270 * Count all UA lockouts in the last 7 days.
271 *
272 * @return string|null
273 */
274 public static function count_ua_lockout_last_7_days(): ?string {
275 $start = strtotime( '-7 days' );
276 $end = time();
277
278 return self::count( $start, $end, self::get_ua_lockout_types() );
279 }
280
281 /**
282 * Count all UA lockouts in the last 24 hours.
283 *
284 * @return string|null
285 */
286 public static function count_ua_lockouts_in_24_hours(): ?string {
287 $start = strtotime( '-24 hours' );
288 $end = time();
289
290 return self::count( $start, $end, self::get_ua_lockout_types() );
291 }
292
293 /**
294 * A shortcut for quickly count ALL lockouts in last 24 hours.
295 *
296 * @return string|null
297 */
298 public static function count_lockout_in_24_hours(): ?string {
299 $start = strtotime( '-24 hours' );
300 $end = time();
301
302 return self::count(
303 $start,
304 $end,
305 self::get_all_lockout_types()
306 );
307 }
308
309 /**
310 * A shortcut for quickly count lockout in last 7 days.
311 *
312 * @return string|null
313 */
314 public static function count_lockout_in_7_days(): ?string {
315 $start = strtotime( '-7 days' );
316 $end = time();
317
318 return self::count(
319 $start,
320 $end,
321 self::get_all_lockout_types()
322 );
323 }
324
325 /**
326 * A shortcut for count lockout in 30 days.
327 *
328 * @return string|null
329 */
330 public static function count_lockout_in_30_days(): ?string {
331 $start = strtotime( '-30 days' );
332 $end = time();
333
334 return self::count(
335 $start,
336 $end,
337 // LOCKOUT_IP_CUSTOM is not taken into account.
338 self::get_all_lockout_types()
339 );
340 }
341
342 /**
343 * Retrieves the date of the last lockout that occurred within the last 30 days.
344 *
345 * @param bool $for_hub (optional) Whether to format the date for the persistent hub. Default is false.
346 *
347 * @return string The formatted date of the last lockout, or 'n/a' if no lockouts were found.
348 */
349 public static function get_last_lockout_date( $for_hub = false ) {
350 $data = self::query_logs(
351 array(
352 'from' => strtotime( '-30 days' ),
353 'to' => time(),
354 ),
355 1,
356 'id',
357 'desc',
358 1
359 );
360 $last = array_shift( $data );
361 if ( ! is_object( $last ) ) {
362 return 'n/a';
363 }
364
365 return $for_hub
366 ? $last->persistent_hub_datetime_format( $last->date )
367 : $last->format_date_time( $last->date );
368 }
369
370 /**
371 * Remove all data.
372 *
373 * @return bool|int
374 */
375 public static function truncate() {
376 $orm = self::get_orm();
377
378 return $orm->get_repository( self::class )
379 ->truncate();
380 }
381
382 /**
383 * Get log types that are attempts/errors (not lockout events).
384 *
385 * @return array
386 */
387 public static function get_attempt_types(): array {
388 return array(
389 self::AUTH_FAIL,
390 self::ERROR_404,
391 self::ERROR_404_IGNORE,
392 self::ERROR_404_XSS,
393 self::ERROR_404_NON_EXISTENT_PLUGIN,
394 self::ERROR_404_NON_EXISTENT_THEME,
395 );
396 }
397
398 /**
399 * Delete only lockout-type log records, preserving attempt/error entries.
400 *
401 * @return int|false Number of rows deleted or false on failure.
402 */
403 public static function delete_lockout_records() {
404 $orm = self::get_orm();
405
406 return $orm->get_repository( self::class )
407 ->where( 'type', 'not in', self::get_attempt_types() )
408 ->delete_all();
409 }
410
411 /**
412 * Remove logs based on timestamp and limit.
413 *
414 * @param int $timestamp The timestamp to filter logs by.
415 * @param int $limit The maximum number of logs to delete.
416 *
417 * @return void
418 */
419 public static function remove_logs( $timestamp, $limit ) {
420 $orm = self::get_orm();
421 $orm->get_repository( self::class )
422 ->where( 'date', '<=', $timestamp )
423 ->order_by( 'id' )
424 ->limit( $limit )
425 ->delete_by_limit();
426 }
427
428 /**
429 * Get log summary.
430 *
431 * @return array
432 */
433 public static function get_summary(): array {
434 // Time.
435 $current_time = time();
436 $today_midnight = strtotime( '-24 hours', $current_time );
437 $first_this_week = strtotime( '-7 days', $current_time );
438 $first_this_month = strtotime( '-30 days', $current_time );
439
440 $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'";
441 $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'";
442
443 // Prepare columns.
444 $select = array(
445 'MAX(date) as lockout_last',
446 'COUNT(*) as lockout_this_month',
447 // 24 hours
448 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
449 "COUNT(IF(date > {$today_midnight} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_today",
450 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
451 "COUNT(IF(date > {$today_midnight} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_today",
452 // 7 days
453 "COUNT(IF(date > {$first_this_week} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_week",
454 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
455 "COUNT(IF(date > {$first_this_week} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_week",
456 // 30 days
457 "COUNT(IF(date > {$first_this_month} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_month",
458 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
459 "COUNT(IF(date > {$first_this_month} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_month",
460 );
461 $select = implode( ',', $select );
462
463 $orm = self::get_orm();
464 $result = $orm->get_repository( self::class )
465 ->select( $select )
466 // LOCKOUT_IP_CUSTOM is not taken into account.
467 ->where( 'type', 'in', self::get_all_lockout_types() )
468 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
469 ->get_results();
470
471 return $result[0] ?? array();
472 }
473
474 /**
475 * Returns the log tag based on the given type.
476 *
477 * @param string $type The type of the log.
478 *
479 * @return string The log tag.
480 */
481 protected static function get_log_tag( $type ): string {
482 switch ( $type ) {
483 case self::LOCKOUT_404:
484 case self::LOCKOUT_404_XSS:
485 case self::LOCKOUT_404_NON_EXISTENT_THEME:
486 case self::LOCKOUT_404_NON_EXISTENT_PLUGIN:
487 case self::ERROR_404:
488 case self::ERROR_404_IGNORE:
489 case self::ERROR_404_XSS:
490 case self::ERROR_404_NON_EXISTENT_THEME:
491 case self::ERROR_404_NON_EXISTENT_PLUGIN:
492 $tag = '404';
493 break;
494 case self::AUTH_FAIL:
495 case self::AUTH_LOCK:
496 $tag = 'login';
497 break;
498 case self::LOCKOUT_IP_CUSTOM:
499 $tag = 'Custom';
500 break;
501 case self::IP_UNLOCK:
502 $tag = 'Unlock';
503 break;
504 case self::LOCKOUT_UA:
505 case self::LOCKOUT_MALICIOUS_BOT:
506 case self::LOCKOUT_FAKE_BOT:
507 default:
508 $tag = 'bots';
509 break;
510 }
511
512 return $tag;
513 }
514
515 /**
516 * Returns the CSS class for the log container based on the given type.
517 *
518 * @param string $type The type of the log.
519 *
520 * @return string The CSS class for the log container.
521 */
522 protected static function get_log_container_class( $type ): string {
523 switch ( $type ) {
524 case self::AUTH_LOCK:
525 case self::LOCKOUT_404:
526 case self::LOCKOUT_404_XSS:
527 case self::LOCKOUT_404_NON_EXISTENT_THEME:
528 case self::LOCKOUT_404_NON_EXISTENT_PLUGIN:
529 case self::LOCKOUT_UA:
530 case self::LOCKOUT_MALICIOUS_BOT:
531 case self::LOCKOUT_FAKE_BOT:
532 $class = 'sui-error';
533 break;
534 case self::AUTH_FAIL:
535 case self::ERROR_404:
536 case self::ERROR_404_IGNORE:
537 case self::ERROR_404_XSS:
538 case self::ERROR_404_NON_EXISTENT_THEME:
539 case self::ERROR_404_NON_EXISTENT_PLUGIN:
540 default:
541 $class = 'sui-warning';
542 break;
543 }
544
545 return $class;
546 }
547
548 /**
549 * Retrieves logs from the database and formats them for display on the frontend.
550 *
551 * @param array $filters An array of filters to apply to the query. Default is an empty array.
552 * The following filters are supported:
553 * - from: The start date for the date range filter.
554 * - to: The end date for the date range filter.
555 * - ip: The IP address to filter by.
556 * - type: The type of log to filter by.
557 * - ban_status: The ban status to filter by.
558 * @param int $paged The page number of the results to retrieve. Default is 1.
559 * @param string $order_by The field to order the results by. Default is 'id'.
560 * @param string $order The order direction of the results. Default is 'desc'.
561 * @param int $page_size The number of results to retrieve per page. Default is 50.
562 * Set to -1 to retrieve all results.
563 *
564 * @return array An array of formatted log entries.
565 */
566 public static function get_logs_and_format(
567 $filters = array(),
568 $paged = 1,
569 $order_by = 'id',
570 $order = 'desc',
571 $page_size = 50
572 ): array {
573 $logs = self::query_logs( $filters, $paged, $order_by, $order, $page_size );
574
575 return self::format_logs( $logs );
576 }
577
578 /**
579 * Get the first log by ID.
580 *
581 * @param int $id The ID of the log.
582 *
583 * @return null|Model
584 */
585 public static function find_by_id( $id ): ?Model {
586 $orm = self::get_orm();
587
588 return $orm->get_repository( self::class )
589 ->where( 'id', $id )
590 ->first();
591 }
592
593 /**
594 * Get logs by an array of IDs.
595 *
596 * @param int[] $ids Array of log IDs.
597 *
598 * @return array
599 */
600 public static function find_by_ids( array $ids ): array {
601 if ( array() === $ids ) {
602 return array();
603 }
604
605 $orm = self::get_orm();
606
607 return $orm->get_repository( self::class )
608 ->where( 'id', 'in', $ids )
609 ->get();
610 }
611
612 /**
613 * Delete current log.
614 */
615 public function delete() {
616 $orm = self::get_orm();
617 $orm->get_repository( self::class )->delete(
618 array(
619 'id' => $this->id,
620 )
621 );
622 }
623
624 /**
625 * Apply ban status filtering based on lockout type.
626 *
627 * @param object $orm The ORM query builder.
628 * @param array $filters The filters array.
629 */
630 private static function apply_ban_status_filter( $orm, $filters ) {
631 $ban_status = $filters['ban_status'];
632 $type = $filters['type'] ?? '';
633 $ua_types = self::get_ua_lockout_types();
634
635 $where = self::ban_status_where( $ban_status );
636
637 if ( 3 === count( $where ) ) {
638 if ( in_array( $type, $ua_types, true ) ) {
639 $orm->where( ...$where );
640 } elseif ( '' === $type ) {
641 $orm->where( 'type', 'in', $ua_types );
642 $orm->where( ...$where );
643 }
644 return;
645 }
646
647 $table_lockout = wd_di()->get( Table_Lockout::class );
648 $bl_model = wd_di()->get( Blacklist_Lockout::class );
649
650 if ( $table_lockout::STATUS_BAN === $ban_status ) {
651 $list = $bl_model->get_list( 'blocklist' );
652 // Empty blocklist means no IPs are banned — force zero results.
653 $orm->where( 'ip', 'in', array() === $list ? array( '' ) : $list );
654 } elseif ( $table_lockout::STATUS_NOT_BAN === $ban_status ) {
655 $list = $bl_model->get_list( 'blocklist' );
656 // Empty blocklist means all IPs are "not banned" — no filter needed.
657 if ( array() !== $list ) {
658 $orm->where( 'ip', 'not in', $list );
659 }
660 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status ) {
661 $list = $bl_model->get_list( 'allowlist' );
662 // Empty allowlist means no IPs are allowlisted — force zero results.
663 $orm->where( 'ip', 'in', array() === $list ? array( '' ) : $list );
664 }
665 }
666
667 /**
668 * Prepare user-agent where condition based on the ban status variant.
669 *
670 * @param string $ban_status_type Ban status type.
671 *
672 * @return array Where condition arguments or empty array.
673 */
674 private static function ban_status_where( $ban_status_type ): array {
675 $table_lockout = wd_di()->get( Table_Lockout::class );
676 $ua_model = wd_di()->get( User_Agent_Lockout::class );
677
678 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
679 $blocklist = $ua_model->get_all_selected_blocklist_ua();
680 if ( array() === $blocklist ) {
681 return array();
682 }
683 $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
684 return array( 'user_agent', 'not regexp', '(?i)' . implode( '|', $escaped ) );
685 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
686 $blocklist = $ua_model->get_all_selected_blocklist_ua();
687 if ( array() === $blocklist ) {
688 return array();
689 }
690 $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
691 return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
692 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
693 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
694 if ( array() === $allowlist ) {
695 return array();
696 }
697 $escaped = array_map( 'preg_quote', $allowlist, array_fill( 0, count( $allowlist ), '' ) );
698 return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
699 }
700
701 return array();
702 }
703
704 /**
705 * Format logs for ready to use on frontend.
706 *
707 * @param array $logs An array of log entries.
708 *
709 * @return array
710 * @since 3.11.0
711 */
712 public static function format_logs( array $logs ): array {
713 $data = array();
714 foreach ( $logs as $item ) {
715 // Escape object properties received from end user.
716 $item->log = sanitize_textarea_field( $item->log );
717 $item->tried = sanitize_textarea_field( $item->tried );
718 $log = $item->export();
719
720 // Escape array keys received from end user.
721 $log['log'] = sanitize_textarea_field( $log['log'] );
722 $log['tried'] = sanitize_textarea_field( $log['tried'] );
723
724 $log['date'] = $item->format_date_time( $item->date );
725 $log['format_date'] = $item->get_date( $item->date );
726 $log['tag'] = self::get_log_tag( $item->type );
727 $log['container_class'] = self::get_log_container_class( $item->type );
728 if ( self::LOCKOUT_UA !== $item->type ) {
729 $log['description'] = sanitize_textarea_field( $item->log );
730 }
731 $data[] = $log;
732 }
733
734 return $data;
735 }
736
737 /**
738 * Determine if the IP should be added to the database based on the timeframe.
739 *
740 * @return bool True if the IP should be added to the database, false otherwise.
741 */
742 public function has_recent_ip_log(): bool {
743 // Ensure IP is set before proceeding.
744 if ( ! is_string( $this->ip ) || '' === trim( $this->ip ) ) {
745 return false;
746 }
747
748 $orm = self::get_orm();
749 // Query the latest log for the current IP.
750 $latest_log = $orm->get_repository( self::class )
751 ->select( 'date' )
752 ->where( 'ip', $this->ip )
753 ->order_by( 'date', 'desc' )
754 ->first();
755
756 if ( null !== $latest_log ) {
757 // Return true if the log is within the 5-minute timeframe.
758 return ( time() - $latest_log->date ) <= 300;
759 }
760
761 return false;
762 }
763
764 /**
765 * Get UA lockout types.
766 *
767 * @return array
768 */
769 public static function get_ua_lockout_types(): array {
770 return array(
771 self::LOCKOUT_UA,
772 self::LOCKOUT_MALICIOUS_BOT,
773 self::LOCKOUT_FAKE_BOT,
774 );
775 }
776 }
777