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

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