| @@ -9,8 +9,9 @@ | ||
| 9 | 9 | |
| 10 | 10 | use WP_Defender\DB; |
| 11 | 11 | use Calotes\Base\Model; |
| 12 | 12 | use WP_Defender\Traits\Formats; |
| 13 | +use WP_Defender\Traits\Country; | |
| 13 | 14 | use WP_Defender\Component\User_Agent; |
| 14 | 15 | use WP_Defender\Component\Table_Lockout; |
| 15 | 16 | use WP_Defender\Model\Setting\Blacklist_Lockout; |
| 16 | 17 | use WP_Defender\Model\Setting\User_Agent_Lockout; |
| @@ -20,8 +21,9 @@ | ||
| 20 | 21 | */ |
| 21 | 22 | class Lockout_Log extends DB { |
| 22 | 23 | |
| 23 | 24 | use Formats; |
| 25 | + use Country; | |
| 24 | 26 | |
| 25 | 27 | // The Firewall log hierarchy. Parent items: 'login', '404', 'bots', 'unlock' and 'custom'. |
| 26 | 28 | // Group of 'login' cases. |
| 27 | 29 | public const AUTH_FAIL = 'auth_fail', AUTH_LOCK = 'auth_lock'; |
| @@ -38,10 +40,8 @@ | ||
| 38 | 40 | public const IP_UNLOCK = 'ip_unlock'; |
| 39 | 41 | // Group of 'custom' cases. |
| 40 | 42 | public const LOCKOUT_IP_CUSTOM = 'custom_lockout'; |
| 41 | 43 | |
| 42 | - public const INFINITE_SCROLL_SIZE = 50; | |
| 43 | - | |
| 44 | 44 | /** |
| 45 | 45 | * Table name. |
| 46 | 46 | * |
| 47 | 47 | * @var string |
| @@ -112,8 +112,34 @@ | ||
| 112 | 112 | */ |
| 113 | 113 | public $country_iso_code; |
| 114 | 114 | |
| 115 | 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 | + /** | |
| 116 | 142 | * Query the logs based on the provided filters and pagination settings. |
| 117 | 143 | * |
| 118 | 144 | * @param array $filters An array of filters to apply to the query. Default is an empty array. |
| 119 | 145 | * The following filters are supported: |
| @@ -234,13 +260,13 @@ | ||
| 234 | 260 | /** |
| 235 | 261 | * Get all lockout types. |
| 236 | 262 | */ |
| 237 | 263 | public static function get_all_lockout_types(): array { |
| 238 | - $arr = self::get_404_lockout_types(); | |
| 239 | - $arr[] = self::AUTH_LOCK; | |
| 240 | - $arr[] = self::LOCKOUT_UA; | |
| 241 | - | |
| 242 | - return $arr; | |
| 264 | + return array_merge( | |
| 265 | + self::get_404_lockout_types(), | |
| 266 | + array( self::AUTH_LOCK ), | |
| 267 | + self::get_ua_lockout_types() | |
| 268 | + ); | |
| 243 | 269 | } |
| 244 | 270 | |
| 245 | 271 | /** |
| 246 | 272 | * Count login lockout in the last 7 days. |
| @@ -266,9 +292,9 @@ | ||
| 266 | 292 | return self::count( $start, $end, self::get_404_lockout_types() ); |
| 267 | 293 | } |
| 268 | 294 | |
| 269 | 295 | /** |
| 270 | - * Count UA lockout in the last 7 days. | |
| 296 | + * Count all UA lockouts in the last 7 days. | |
| 271 | 297 | * |
| 272 | 298 | * @return string|null |
| 273 | 299 | */ |
| 274 | 300 | public static function count_ua_lockout_last_7_days(): ?string { |
| @@ -274,16 +300,28 @@ | ||
| 274 | 300 | public static function count_ua_lockout_last_7_days(): ?string { |
| 275 | 301 | $start = strtotime( '-7 days' ); |
| 276 | 302 | $end = time(); |
| 277 | 303 | |
| 278 | - return self::count( $start, $end, self::LOCKOUT_UA ); | |
| 304 | + return self::count( $start, $end, self::get_ua_lockout_types() ); | |
| 279 | 305 | } |
| 280 | 306 | |
| 281 | 307 | /** |
| 282 | - * A shortcut for quickly count lockout in last 24 hours. | |
| 308 | + * Count all UA lockouts in the last 24 hours. | |
| 283 | 309 | * |
| 284 | 310 | * @return string|null |
| 285 | 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 | + */ | |
| 286 | 324 | public static function count_lockout_in_24_hours(): ?string { |
| 287 | 325 | $start = strtotime( '-24 hours' ); |
| 288 | 326 | $end = time(); |
| 289 | 327 | |
| @@ -367,8 +405,37 @@ | ||
| 367 | 405 | ->truncate(); |
| 368 | 406 | } |
| 369 | 407 | |
| 370 | 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 | + /** | |
| 371 | 438 | * Remove logs based on timestamp and limit. |
| 372 | 439 | * |
| 373 | 440 | * @param int $timestamp The timestamp to filter logs by. |
| 374 | 441 | * @param int $limit The maximum number of logs to delete. |
| @@ -395,8 +462,11 @@ | ||
| 395 | 462 | $today_midnight = strtotime( '-24 hours', $current_time ); |
| 396 | 463 | $first_this_week = strtotime( '-7 days', $current_time ); |
| 397 | 464 | $first_this_month = strtotime( '-30 days', $current_time ); |
| 398 | 465 | |
| 466 | + $ua_types_sql = "'" . implode( "', '", self::get_ua_lockout_types() ) . "'"; | |
| 467 | + $types_404_sql = "'" . implode( "', '", self::get_404_lockout_types() ) . "'"; | |
| 468 | + | |
| 399 | 469 | // Prepare columns. |
| 400 | 470 | $select = array( |
| 401 | 471 | 'MAX(date) as lockout_last', |
| 402 | 472 | 'COUNT(*) as lockout_this_month', |
| @@ -401,19 +471,19 @@ | ||
| 401 | 471 | 'MAX(date) as lockout_last', |
| 402 | 472 | 'COUNT(*) as lockout_this_month', |
| 403 | 473 | // 24 hours |
| 404 | 474 | "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today", |
| 405 | - "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", | |
| 475 | + "COUNT(IF(date > {$today_midnight} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_today", | |
| 406 | 476 | "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today", |
| 407 | - "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_today", | |
| 477 | + "COUNT(IF(date > {$today_midnight} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_today", | |
| 408 | 478 | // 7 days |
| 409 | - "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", | |
| 479 | + "COUNT(IF(date > {$first_this_week} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_week", | |
| 410 | 480 | "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week", |
| 411 | - "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_week", | |
| 481 | + "COUNT(IF(date > {$first_this_week} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_week", | |
| 412 | 482 | // 30 days |
| 413 | - "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", | |
| 483 | + "COUNT(IF(date > {$first_this_month} AND type IN ({$types_404_sql}), 1, NULL)) as lockout_404_this_month", | |
| 414 | 484 | "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month", |
| 415 | - "COUNT(IF(date > {$first_this_month} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_month", | |
| 485 | + "COUNT(IF(date > {$first_this_month} AND type IN ({$ua_types_sql}), 1, NULL)) as lockout_ua_this_month", | |
| 416 | 486 | ); |
| 417 | 487 | $select = implode( ',', $select ); |
| 418 | 488 | |
| 419 | 489 | $orm = self::get_orm(); |
| @@ -587,11 +657,15 @@ | ||
| 587 | 657 | $ban_status = $filters['ban_status']; |
| 588 | 658 | $type = $filters['type'] ?? ''; |
| 589 | 659 | $ua_types = self::get_ua_lockout_types(); |
| 590 | 660 | |
| 591 | - if ( in_array( $type, $ua_types, true ) ) { | |
| 592 | - $where = self::ban_status_where( $ban_status ); | |
| 593 | - if ( 3 === count( $where ) ) { | |
| 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 ); | |
| 594 | 668 | $orm->where( ...$where ); |
| 595 | 669 | } |
| 596 | 670 | return; |
| 597 | 671 | } |