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
defender-security / src / model / class-lockout-log.php

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

803 lines 22.5 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\Traits\Country;
14 use WP_Defender\Component\User_Agent;
15 use WP_Defender\Component\Table_Lockout;
16 use WP_Defender\Model\Setting\Blacklist_Lockout;
17 use WP_Defender\Model\Setting\User_Agent_Lockout;
18
19 /**
20 * Model for the lockout log table.
21 */
22 class Lockout_Log extends DB {
23
24 use Formats;
25 use Country;
26
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.
40 public const IP_UNLOCK = 'ip_unlock';
41 // Group of 'custom' cases.
42 public const LOCKOUT_IP_CUSTOM = 'custom_lockout';
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 * 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 /**
142 * Query the logs based on the provided filters and pagination settings.
143 *
144 * @param array $filters An array of filters to apply to the query. Default is an empty array.
145 * The following filters are supported:
146 * - from: The start date for the date range filter.
147 * - to: The end date for the date range filter.
148 * - ip: The IP address to filter by.
149 * - type: The type of log to filter by.
150 * - ban_status: The ban status to filter by.
151 * @param int $paged The page number of the results to retrieve. Default is 1.
152 * @param string $order_by The field to order the results by. Default is 'id'.
153 * @param string $order The order direction of the results. Default is 'desc'.
154 * @param int $page_size The number of results to retrieve per page. Default is 50.
155 * Set to -1 to retrieve all results.
156 *
157 * @return array An array of Lockout_Log objects representing the queried logs.
158 */
159 public static function query_logs(
160 $filters = array(),
161 $paged = 1,
162 $order_by = 'id',
163 $order = 'desc',
164 $page_size = 50
165 ): array {
166 $orm = self::get_orm();
167 $orm->get_repository( self::class )
168 ->where(
169 'date',
170 'between',
171 array(
172 $filters['from'],
173 $filters['to'],
174 )
175 );
176
177 if ( isset( $filters['ip'] ) && '' !== $filters['ip'] ) {
178 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
179 }
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'] ) {
184 $orm->where( 'type', $filters['type'] );
185 }
186
187 if ( isset( $filters['ban_status'] ) && '' !== $filters['ban_status'] ) {
188 self::apply_ban_status_filter( $orm, $filters );
189 }
190
191 if ( '' !== $order_by && '' !== $order ) {
192 $orm->order_by( $order_by, $order );
193 }
194 if ( $page_size > 0 ) {
195 $offset = ( $paged - 1 ) * $page_size;
196 $orm->limit( $page_size, $offset );
197 }
198
199 return $orm->get();
200 }
201
202 /**
203 * Count the number of records in the database table based on the provided filters.
204 *
205 * @param mixed $date_from The start date for the date range filter.
206 * @param mixed $date_to The end date for the date range filter.
207 * @param mixed $type The type of log to filter by.
208 * @param string $ip The IP address to filter by. Default is an empty string.
209 * @param array $filters An array of additional filters to apply to the query. Default is an empty array.
210 * The following filters are supported:
211 * - ban_status: The ban status to filter by.
212 *
213 * @return string|null The number of records matching the provided filters, or null if an error occurred.
214 */
215 public static function count( $date_from, $date_to, $type, $ip = '', $filters = array() ): ?string {
216 $orm = self::get_orm();
217 $orm->get_repository( self::class )
218 ->where(
219 'date',
220 'between',
221 array(
222 $date_from,
223 $date_to,
224 )
225 );
226
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 );
231 }
232
233 if ( is_string( $ip ) && '' !== trim( $ip ) ) {
234 $orm->where( 'ip', 'like', "%$ip%" );
235 }
236
237 if ( isset( $filters['user_agent'] ) && '' !== trim( $filters['user_agent'] ) ) {
238 $orm->where( 'user_agent', 'like', '%' . $filters['user_agent'] . '%' );
239 }
240
241 if ( isset( $filters['ban_status'] ) && '' !== trim( $filters['ban_status'] ) ) {
242 self::apply_ban_status_filter( $orm, array_merge( $filters, array( 'type' => $type ) ) );
243 }
244
245 return $orm->count();
246 }
247
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 /**
272 * Count login lockout in the last 7 days.
273 *
274 * @return string|null
275 */
276 public static function count_login_lockout_last_7_days(): ?string {
277 $start = strtotime( '-7 days' );
278 $end = time();
279
280 return self::count( $start, $end, self::AUTH_LOCK );
281 }
282
283 /**
284 * Count 404 lockout in the last 7 days.
285 *
286 * @return string|null
287 */
288 public static function count_404_lockout_last_7_days(): ?string {
289 $start = strtotime( '-7 days' );
290 $end = time();
291
292 return self::count( $start, $end, self::get_404_lockout_types() );
293 }
294
295 /**
296 * Count all UA lockouts in the last 7 days.
297 *
298 * @return string|null
299 */
300 public static function count_ua_lockout_last_7_days(): ?string {
301 $start = strtotime( '-7 days' );
302 $end = time();
303
304 return self::count( $start, $end, self::get_ua_lockout_types() );
305 }
306
307 /**
308 * Count all UA lockouts in the last 24 hours.
309 *
310 * @return string|null
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 */
324 public static function count_lockout_in_24_hours(): ?string {
325 $start = strtotime( '-24 hours' );
326 $end = time();
327
328 return self::count(
329 $start,
330 $end,
331 self::get_all_lockout_types()
332 );
333 }
334
335 /**
336 * A shortcut for quickly count lockout in last 7 days.
337 *
338 * @return string|null
339 */
340 public static function count_lockout_in_7_days(): ?string {
341 $start = strtotime( '-7 days' );
342 $end = time();
343
344 return self::count(
345 $start,
346 $end,
347 self::get_all_lockout_types()
348 );
349 }
350
351 /**
352 * A shortcut for count lockout in 30 days.
353 *
354 * @return string|null
355 */
356 public static function count_lockout_in_30_days(): ?string {
357 $start = strtotime( '-30 days' );
358 $end = time();
359
360 return self::count(
361 $start,
362 $end,
363 // LOCKOUT_IP_CUSTOM is not taken into account.
364 self::get_all_lockout_types()
365 );
366 }
367
368 /**
369 * Retrieves the date of the last lockout that occurred within the last 30 days.
370 *
371 * @param bool $for_hub (optional) Whether to format the date for the persistent hub. Default is false.
372 *
373 * @return string The formatted date of the last lockout, or 'n/a' if no lockouts were found.
374 */
375 public static function get_last_lockout_date( $for_hub = false ) {
376 $data = self::query_logs(
377 array(
378 'from' => strtotime( '-30 days' ),
379 'to' => time(),
380 ),
381 1,
382 'id',
383 'desc',
384 1
385 );
386 $last = array_shift( $data );
387 if ( ! is_object( $last ) ) {
388 return 'n/a';
389 }
390
391 return $for_hub
392 ? $last->persistent_hub_datetime_format( $last->date )
393 : $last->format_date_time( $last->date );
394 }
395
396 /**
397 * Remove all data.
398 *
399 * @return bool|int
400 */
401 public static function truncate() {
402 $orm = self::get_orm();
403
404 return $orm->get_repository( self::class )
405 ->truncate();
406 }
407
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 /**
438 * Remove logs based on timestamp and limit.
439 *
440 * @param int $timestamp The timestamp to filter logs by.
441 * @param int $limit The maximum number of logs to delete.
442 *
443 * @return void
444 */
445 public static function remove_logs( $timestamp, $limit ) {
446 $orm = self::get_orm();
447 $orm->get_repository( self::class )
448 ->where( 'date', '<=', $timestamp )
449 ->order_by( 'id' )
450 ->limit( $limit )
451 ->delete_by_limit();
452 }
453
454 /**
455 * Get log summary.
456 *
457 * @return array
458 */
459 public static function get_summary(): array {
460 // Time.
461 $current_time = time();
462 $today_midnight = strtotime( '-24 hours', $current_time );
463 $first_this_week = strtotime( '-7 days', $current_time );
464 $first_this_month = strtotime( '-30 days', $current_time );
465
466 $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'";
467 $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'";
468
469 // Prepare columns.
470 $select = array(
471 'MAX(date) as lockout_last',
472 'COUNT(*) as lockout_this_month',
473 // 24 hours
474 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
475 "COUNT(IF(date > {$today_midnight} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_today",
476 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
477 "COUNT(IF(date > {$today_midnight} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_today",
478 // 7 days
479 "COUNT(IF(date > {$first_this_week} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_week",
480 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
481 "COUNT(IF(date > {$first_this_week} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_week",
482 // 30 days
483 "COUNT(IF(date > {$first_this_month} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_month",
484 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
485 "COUNT(IF(date > {$first_this_month} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_month",
486 );
487 $select = implode( ',', $select );
488
489 $orm = self::get_orm();
490 $result = $orm->get_repository( self::class )
491 ->select( $select )
492 // LOCKOUT_IP_CUSTOM is not taken into account.
493 ->where( 'type', 'in', self::get_all_lockout_types() )
494 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
495 ->get_results();
496
497 return $result[0] ?? array();
498 }
499
500 /**
501 * Returns the log tag based on the given type.
502 *
503 * @param string $type The type of the log.
504 *
505 * @return string The log tag.
506 */
507 protected static function get_log_tag( $type ): string {
508 switch ( $type ) {
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:
513 case self::ERROR_404:
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:
518 $tag = '404';
519 break;
520 case self::AUTH_FAIL:
521 case self::AUTH_LOCK:
522 $tag = 'login';
523 break;
524 case self::LOCKOUT_IP_CUSTOM:
525 $tag = 'Custom';
526 break;
527 case self::IP_UNLOCK:
528 $tag = 'Unlock';
529 break;
530 case self::LOCKOUT_UA:
531 case self::LOCKOUT_MALICIOUS_BOT:
532 case self::LOCKOUT_FAKE_BOT:
533 default:
534 $tag = 'bots';
535 break;
536 }
537
538 return $tag;
539 }
540
541 /**
542 * Returns the CSS class for the log container based on the given type.
543 *
544 * @param string $type The type of the log.
545 *
546 * @return string The CSS class for the log container.
547 */
548 protected static function get_log_container_class( $type ): string {
549 switch ( $type ) {
550 case self::AUTH_LOCK:
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:
555 case self::LOCKOUT_UA:
556 case self::LOCKOUT_MALICIOUS_BOT:
557 case self::LOCKOUT_FAKE_BOT:
558 $class = 'sui-error';
559 break;
560 case self::AUTH_FAIL:
561 case self::ERROR_404:
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:
566 default:
567 $class = 'sui-warning';
568 break;
569 }
570
571 return $class;
572 }
573
574 /**
575 * Retrieves logs from the database and formats them for display on the frontend.
576 *
577 * @param array $filters An array of filters to apply to the query. Default is an empty array.
578 * The following filters are supported:
579 * - from: The start date for the date range filter.
580 * - to: The end date for the date range filter.
581 * - ip: The IP address to filter by.
582 * - type: The type of log to filter by.
583 * - ban_status: The ban status to filter by.
584 * @param int $paged The page number of the results to retrieve. Default is 1.
585 * @param string $order_by The field to order the results by. Default is 'id'.
586 * @param string $order The order direction of the results. Default is 'desc'.
587 * @param int $page_size The number of results to retrieve per page. Default is 50.
588 * Set to -1 to retrieve all results.
589 *
590 * @return array An array of formatted log entries.
591 */
592 public static function get_logs_and_format(
593 $filters = array(),
594 $paged = 1,
595 $order_by = 'id',
596 $order = 'desc',
597 $page_size = 50
598 ): array {
599 $logs = self::query_logs( $filters, $paged, $order_by, $order, $page_size );
600
601 return self::format_logs( $logs );
602 }
603
604 /**
605 * Get the first log by ID.
606 *
607 * @param int $id The ID of the log.
608 *
609 * @return null|Model
610 */
611 public static function find_by_id( $id ): ?Model {
612 $orm = self::get_orm();
613
614 return $orm->get_repository( self::class )
615 ->where( 'id', $id )
616 ->first();
617 }
618
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 /**
639 * Delete current log.
640 */
641 public function delete() {
642 $orm = self::get_orm();
643 $orm->get_repository( self::class )->delete(
644 array(
645 'id' => $this->id,
646 )
647 );
648 }
649
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 /**
694 * Prepare user-agent where condition based on the ban status variant.
695 *
696 * @param string $ban_status_type Ban status type.
697 *
698 * @return array Where condition arguments or empty array.
699 */
700 private static function ban_status_where( $ban_status_type ): array {
701 $table_lockout = wd_di()->get( Table_Lockout::class );
702 $ua_model = wd_di()->get( User_Agent_Lockout::class );
703
704 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
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 ) );
711 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
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 ) );
718 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
719 $allowlist = $ua_model->get_lockout_list( '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 ) );
725 }
726
727 return array();
728 }
729
730 /**
731 * Format logs for ready to use on frontend.
732 *
733 * @param array $logs An array of log entries.
734 *
735 * @return array
736 * @since 3.11.0
737 */
738 public static function format_logs( array $logs ): array {
739 $data = array();
740 foreach ( $logs as $item ) {
741 // Escape object properties received from end user.
742 $item->log = sanitize_textarea_field( $item->log );
743 $item->tried = sanitize_textarea_field( $item->tried );
744 $log = $item->export();
745
746 // Escape array keys received from end user.
747 $log['log'] = sanitize_textarea_field( $log['log'] );
748 $log['tried'] = sanitize_textarea_field( $log['tried'] );
749
750 $log['date'] = $item->format_date_time( $item->date );
751 $log['format_date'] = $item->get_date( $item->date );
752 $log['tag'] = self::get_log_tag( $item->type );
753 $log['container_class'] = self::get_log_container_class( $item->type );
754 if ( self::LOCKOUT_UA !== $item->type ) {
755 $log['description'] = sanitize_textarea_field( $item->log );
756 }
757 $data[] = $log;
758 }
759
760 return $data;
761 }
762
763 /**
764 * Determine if the IP should be added to the database based on the timeframe.
765 *
766 * @return bool True if the IP should be added to the database, false otherwise.
767 */
768 public function has_recent_ip_log(): bool {
769 // Ensure IP is set before proceeding.
770 if ( ! is_string( $this->ip ) || '' === trim( $this->ip ) ) {
771 return false;
772 }
773
774 $orm = self::get_orm();
775 // Query the latest log for the current IP.
776 $latest_log = $orm->get_repository( self::class )
777 ->select( 'date' )
778 ->where( 'ip', $this->ip )
779 ->order_by( 'date', 'desc' )
780 ->first();
781
782 if ( null !== $latest_log ) {
783 // Return true if the log is within the 5-minute timeframe.
784 return ( time() - $latest_log->date ) <= 300;
785 }
786
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 );
801 }
802 }
803