PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.1.0
Defender Security – Malware Scanner, Login Security & Firewall v6.1.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.1.0, at src/model/class-lockout-log.php

765 lines 21.4 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 UA lockout 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 * A shortcut for quickly count lockout in last 24 hours.
283 *
284 * @return string|null
285 */
286 public static function count_lockout_in_24_hours(): ?string {
287 $start = strtotime( '-24 hours' );
288 $end = time();
289
290 return self::count(
291 $start,
292 $end,
293 self::get_all_lockout_types()
294 );
295 }
296
297 /**
298 * A shortcut for quickly count lockout in last 7 days.
299 *
300 * @return string|null
301 */
302 public static function count_lockout_in_7_days(): ?string {
303 $start = strtotime( '-7 days' );
304 $end = time();
305
306 return self::count(
307 $start,
308 $end,
309 self::get_all_lockout_types()
310 );
311 }
312
313 /**
314 * A shortcut for count lockout in 30 days.
315 *
316 * @return string|null
317 */
318 public static function count_lockout_in_30_days(): ?string {
319 $start = strtotime( '-30 days' );
320 $end = time();
321
322 return self::count(
323 $start,
324 $end,
325 // LOCKOUT_IP_CUSTOM is not taken into account.
326 self::get_all_lockout_types()
327 );
328 }
329
330 /**
331 * Retrieves the date of the last lockout that occurred within the last 30 days.
332 *
333 * @param bool $for_hub (optional) Whether to format the date for the persistent hub. Default is false.
334 *
335 * @return string The formatted date of the last lockout, or 'n/a' if no lockouts were found.
336 */
337 public static function get_last_lockout_date( $for_hub = false ) {
338 $data = self::query_logs(
339 array(
340 'from' => strtotime( '-30 days' ),
341 'to' => time(),
342 ),
343 1,
344 'id',
345 'desc',
346 1
347 );
348 $last = array_shift( $data );
349 if ( ! is_object( $last ) ) {
350 return 'n/a';
351 }
352
353 return $for_hub
354 ? $last->persistent_hub_datetime_format( $last->date )
355 : $last->format_date_time( $last->date );
356 }
357
358 /**
359 * Remove all data.
360 *
361 * @return bool|int
362 */
363 public static function truncate() {
364 $orm = self::get_orm();
365
366 return $orm->get_repository( self::class )
367 ->truncate();
368 }
369
370 /**
371 * Get log types that are attempts/errors (not lockout events).
372 *
373 * @return array
374 */
375 public static function get_attempt_types(): array {
376 return array(
377 self::AUTH_FAIL,
378 self::ERROR_404,
379 self::ERROR_404_IGNORE,
380 self::ERROR_404_XSS,
381 self::ERROR_404_NON_EXISTENT_PLUGIN,
382 self::ERROR_404_NON_EXISTENT_THEME,
383 );
384 }
385
386 /**
387 * Delete only lockout-type log records, preserving attempt/error entries.
388 *
389 * @return int|false Number of rows deleted or false on failure.
390 */
391 public static function delete_lockout_records() {
392 $orm = self::get_orm();
393
394 return $orm->get_repository( self::class )
395 ->where( 'type', 'not in', self::get_attempt_types() )
396 ->delete_all();
397 }
398
399 /**
400 * Remove logs based on timestamp and limit.
401 *
402 * @param int $timestamp The timestamp to filter logs by.
403 * @param int $limit The maximum number of logs to delete.
404 *
405 * @return void
406 */
407 public static function remove_logs( $timestamp, $limit ) {
408 $orm = self::get_orm();
409 $orm->get_repository( self::class )
410 ->where( 'date', '<=', $timestamp )
411 ->order_by( 'id' )
412 ->limit( $limit )
413 ->delete_by_limit();
414 }
415
416 /**
417 * Get log summary.
418 *
419 * @return array
420 */
421 public static function get_summary(): array {
422 // Time.
423 $current_time = time();
424 $today_midnight = strtotime( '-24 hours', $current_time );
425 $first_this_week = strtotime( '-7 days', $current_time );
426 $first_this_month = strtotime( '-30 days', $current_time );
427
428 $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'";
429 $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'";
430
431 // Prepare columns.
432 $select = array(
433 'MAX(date) as lockout_last',
434 'COUNT(*) as lockout_this_month',
435 // 24 hours
436 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
437 "COUNT(IF(date > {$today_midnight} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_today",
438 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
439 "COUNT(IF(date > {$today_midnight} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_today",
440 // 7 days
441 "COUNT(IF(date > {$first_this_week} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_week",
442 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
443 "COUNT(IF(date > {$first_this_week} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_week",
444 // 30 days
445 "COUNT(IF(date > {$first_this_month} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_month",
446 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
447 "COUNT(IF(date > {$first_this_month} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_month",
448 );
449 $select = implode( ',', $select );
450
451 $orm = self::get_orm();
452 $result = $orm->get_repository( self::class )
453 ->select( $select )
454 // LOCKOUT_IP_CUSTOM is not taken into account.
455 ->where( 'type', 'in', self::get_all_lockout_types() )
456 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
457 ->get_results();
458
459 return $result[0] ?? array();
460 }
461
462 /**
463 * Returns the log tag based on the given type.
464 *
465 * @param string $type The type of the log.
466 *
467 * @return string The log tag.
468 */
469 protected static function get_log_tag( $type ): string {
470 switch ( $type ) {
471 case self::LOCKOUT_404:
472 case self::LOCKOUT_404_XSS:
473 case self::LOCKOUT_404_NON_EXISTENT_THEME:
474 case self::LOCKOUT_404_NON_EXISTENT_PLUGIN:
475 case self::ERROR_404:
476 case self::ERROR_404_IGNORE:
477 case self::ERROR_404_XSS:
478 case self::ERROR_404_NON_EXISTENT_THEME:
479 case self::ERROR_404_NON_EXISTENT_PLUGIN:
480 $tag = '404';
481 break;
482 case self::AUTH_FAIL:
483 case self::AUTH_LOCK:
484 $tag = 'login';
485 break;
486 case self::LOCKOUT_IP_CUSTOM:
487 $tag = 'Custom';
488 break;
489 case self::IP_UNLOCK:
490 $tag = 'Unlock';
491 break;
492 case self::LOCKOUT_UA:
493 case self::LOCKOUT_MALICIOUS_BOT:
494 case self::LOCKOUT_FAKE_BOT:
495 default:
496 $tag = 'bots';
497 break;
498 }
499
500 return $tag;
501 }
502
503 /**
504 * Returns the CSS class for the log container based on the given type.
505 *
506 * @param string $type The type of the log.
507 *
508 * @return string The CSS class for the log container.
509 */
510 protected static function get_log_container_class( $type ): string {
511 switch ( $type ) {
512 case self::AUTH_LOCK:
513 case self::LOCKOUT_404:
514 case self::LOCKOUT_404_XSS:
515 case self::LOCKOUT_404_NON_EXISTENT_THEME:
516 case self::LOCKOUT_404_NON_EXISTENT_PLUGIN:
517 case self::LOCKOUT_UA:
518 case self::LOCKOUT_MALICIOUS_BOT:
519 case self::LOCKOUT_FAKE_BOT:
520 $class = 'sui-error';
521 break;
522 case self::AUTH_FAIL:
523 case self::ERROR_404:
524 case self::ERROR_404_IGNORE:
525 case self::ERROR_404_XSS:
526 case self::ERROR_404_NON_EXISTENT_THEME:
527 case self::ERROR_404_NON_EXISTENT_PLUGIN:
528 default:
529 $class = 'sui-warning';
530 break;
531 }
532
533 return $class;
534 }
535
536 /**
537 * Retrieves logs from the database and formats them for display on the frontend.
538 *
539 * @param array $filters An array of filters to apply to the query. Default is an empty array.
540 * The following filters are supported:
541 * - from: The start date for the date range filter.
542 * - to: The end date for the date range filter.
543 * - ip: The IP address to filter by.
544 * - type: The type of log to filter by.
545 * - ban_status: The ban status to filter by.
546 * @param int $paged The page number of the results to retrieve. Default is 1.
547 * @param string $order_by The field to order the results by. Default is 'id'.
548 * @param string $order The order direction of the results. Default is 'desc'.
549 * @param int $page_size The number of results to retrieve per page. Default is 50.
550 * Set to -1 to retrieve all results.
551 *
552 * @return array An array of formatted log entries.
553 */
554 public static function get_logs_and_format(
555 $filters = array(),
556 $paged = 1,
557 $order_by = 'id',
558 $order = 'desc',
559 $page_size = 50
560 ): array {
561 $logs = self::query_logs( $filters, $paged, $order_by, $order, $page_size );
562
563 return self::format_logs( $logs );
564 }
565
566 /**
567 * Get the first log by ID.
568 *
569 * @param int $id The ID of the log.
570 *
571 * @return null|Model
572 */
573 public static function find_by_id( $id ): ?Model {
574 $orm = self::get_orm();
575
576 return $orm->get_repository( self::class )
577 ->where( 'id', $id )
578 ->first();
579 }
580
581 /**
582 * Get logs by an array of IDs.
583 *
584 * @param int[] $ids Array of log IDs.
585 *
586 * @return array
587 */
588 public static function find_by_ids( array $ids ): array {
589 if ( array() === $ids ) {
590 return array();
591 }
592
593 $orm = self::get_orm();
594
595 return $orm->get_repository( self::class )
596 ->where( 'id', 'in', $ids )
597 ->get();
598 }
599
600 /**
601 * Delete current log.
602 */
603 public function delete() {
604 $orm = self::get_orm();
605 $orm->get_repository( self::class )->delete(
606 array(
607 'id' => $this->id,
608 )
609 );
610 }
611
612 /**
613 * Apply ban status filtering based on lockout type.
614 *
615 * @param object $orm The ORM query builder.
616 * @param array $filters The filters array.
617 */
618 private static function apply_ban_status_filter( $orm, $filters ) {
619 $ban_status = $filters['ban_status'];
620 $type = $filters['type'] ?? '';
621 $ua_types = self::get_ua_lockout_types();
622
623 $where = self::ban_status_where( $ban_status );
624
625 if ( 3 === count( $where ) ) {
626 if ( in_array( $type, $ua_types, true ) ) {
627 $orm->where( ...$where );
628 } elseif ( '' === $type ) {
629 $orm->where( 'type', 'in', $ua_types );
630 $orm->where( ...$where );
631 }
632 return;
633 }
634
635 $table_lockout = wd_di()->get( Table_Lockout::class );
636 $bl_model = wd_di()->get( Blacklist_Lockout::class );
637
638 if ( $table_lockout::STATUS_BAN === $ban_status ) {
639 $list = $bl_model->get_list( 'blocklist' );
640 // Empty blocklist means no IPs are banned — force zero results.
641 $orm->where( 'ip', 'in', array() === $list ? array( '' ) : $list );
642 } elseif ( $table_lockout::STATUS_NOT_BAN === $ban_status ) {
643 $list = $bl_model->get_list( 'blocklist' );
644 // Empty blocklist means all IPs are "not banned" — no filter needed.
645 if ( array() !== $list ) {
646 $orm->where( 'ip', 'not in', $list );
647 }
648 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status ) {
649 $list = $bl_model->get_list( 'allowlist' );
650 // Empty allowlist means no IPs are allowlisted — force zero results.
651 $orm->where( 'ip', 'in', array() === $list ? array( '' ) : $list );
652 }
653 }
654
655 /**
656 * Prepare user-agent where condition based on the ban status variant.
657 *
658 * @param string $ban_status_type Ban status type.
659 *
660 * @return array Where condition arguments or empty array.
661 */
662 private static function ban_status_where( $ban_status_type ): array {
663 $table_lockout = wd_di()->get( Table_Lockout::class );
664 $ua_model = wd_di()->get( User_Agent_Lockout::class );
665
666 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
667 $blocklist = $ua_model->get_all_selected_blocklist_ua();
668 if ( array() === $blocklist ) {
669 return array();
670 }
671 $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
672 return array( 'user_agent', 'not regexp', '(?i)' . implode( '|', $escaped ) );
673 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
674 $blocklist = $ua_model->get_all_selected_blocklist_ua();
675 if ( array() === $blocklist ) {
676 return array();
677 }
678 $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
679 return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
680 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
681 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
682 if ( array() === $allowlist ) {
683 return array();
684 }
685 $escaped = array_map( 'preg_quote', $allowlist, array_fill( 0, count( $allowlist ), '' ) );
686 return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
687 }
688
689 return array();
690 }
691
692 /**
693 * Format logs for ready to use on frontend.
694 *
695 * @param array $logs An array of log entries.
696 *
697 * @return array
698 * @since 3.11.0
699 */
700 public static function format_logs( array $logs ): array {
701 $data = array();
702 foreach ( $logs as $item ) {
703 // Escape object properties received from end user.
704 $item->log = sanitize_textarea_field( $item->log );
705 $item->tried = sanitize_textarea_field( $item->tried );
706 $log = $item->export();
707
708 // Escape array keys received from end user.
709 $log['log'] = sanitize_textarea_field( $log['log'] );
710 $log['tried'] = sanitize_textarea_field( $log['tried'] );
711
712 $log['date'] = $item->format_date_time( $item->date );
713 $log['format_date'] = $item->get_date( $item->date );
714 $log['tag'] = self::get_log_tag( $item->type );
715 $log['container_class'] = self::get_log_container_class( $item->type );
716 if ( self::LOCKOUT_UA !== $item->type ) {
717 $log['description'] = sanitize_textarea_field( $item->log );
718 }
719 $data[] = $log;
720 }
721
722 return $data;
723 }
724
725 /**
726 * Determine if the IP should be added to the database based on the timeframe.
727 *
728 * @return bool True if the IP should be added to the database, false otherwise.
729 */
730 public function has_recent_ip_log(): bool {
731 // Ensure IP is set before proceeding.
732 if ( ! is_string( $this->ip ) || '' === trim( $this->ip ) ) {
733 return false;
734 }
735
736 $orm = self::get_orm();
737 // Query the latest log for the current IP.
738 $latest_log = $orm->get_repository( self::class )
739 ->select( 'date' )
740 ->where( 'ip', $this->ip )
741 ->order_by( 'date', 'desc' )
742 ->first();
743
744 if ( null !== $latest_log ) {
745 // Return true if the log is within the 5-minute timeframe.
746 return ( time() - $latest_log->date ) <= 300;
747 }
748
749 return false;
750 }
751
752 /**
753 * Get UA lockout types.
754 *
755 * @return array
756 */
757 public static function get_ua_lockout_types(): array {
758 return array(
759 self::LOCKOUT_UA,
760 self::LOCKOUT_MALICIOUS_BOT,
761 self::LOCKOUT_FAKE_BOT,
762 );
763 }
764 }
765