ban_level, $level ); $wpdb->update( Argus_DB::table( 'bans' ), array( 'source' => $source, 'reason' => $reason, 'evidence' => wp_json_encode( $evidence ), 'ban_level' => $new_level, 'expires_at' => $ttl ? gmdate( 'Y-m-d H:i:s', strtotime( $now ) + self::ttl_for_level( $new_level ) ) : null, 'recovery_eligible_at' => $ttl ? gmdate( 'Y-m-d H:i:s', strtotime( $now ) + self::ttl_for_level( $new_level ) ) : null, ), array( 'id' => $existing->id ), array( '%s', '%s', '%s', '%s', '%s', '%s' ), array( '%d' ) ); $id = (int) $existing->id; } else { $wpdb->insert( Argus_DB::table( 'bans' ), array( 'ip' => $ip, 'source' => $source, 'reason' => $reason, 'evidence' => wp_json_encode( $evidence ), 'ban_level' => $level, 'created_at' => $now, 'expires_at' => $ttl ? gmdate( 'Y-m-d H:i:s', strtotime( $now ) + $ttl ) : null, 'recovery_eligible_at' => $ttl ? gmdate( 'Y-m-d H:i:s', strtotime( $now ) + $ttl ) : null, ), array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ); $id = (int) $wpdb->insert_id; } self::invalidate_cache( $ip ); Argus_Events::record( 'ban', 'high', sprintf( 'Banned %s (%s)', $ip, $reason ), array( 'ip' => $ip, 'source' => $source, 'reason' => $reason, 'evidence' => $evidence, 'level' => $level ), $ip ); do_action( 'argus_wpd_ip_banned', $ip, $source, $reason, $level ); return $id; } public static function is_banned( $ip ) { if ( array_key_exists( $ip, self::$request_cache ) ) { return self::$request_cache[ $ip ]; } $cache_key = self::cache_key( $ip ); $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { self::$request_cache[ $ip ] = (bool) $cached; return self::$request_cache[ $ip ]; } $result = null !== self::active_ban( $ip ); self::$request_cache[ $ip ] = $result; wp_cache_set( $cache_key, $result ? 'yes' : 'no', self::CACHE_GROUP, self::CACHE_TTL ); return $result; } protected static function cache_key( $ip ) { return 'is_banned_' . md5( $ip ); } protected static function invalidate_cache( $ip ) { unset( self::$request_cache[ $ip ] ); wp_cache_delete( self::cache_key( $ip ), self::CACHE_GROUP ); } public static function active_ban( $ip ) { global $wpdb; $table = Argus_DB::table( 'bans' ); $now = current_time( 'mysql', true ); return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE ip = %s AND lifted_at IS NULL AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC LIMIT 1", // phpcs:ignore $ip, $now ) ); } public static function lift( $ban_id ) { global $wpdb; $table = Argus_DB::table( 'bans' ); $ip = $wpdb->get_var( $wpdb->prepare( "SELECT ip FROM {$table} WHERE id = %d", $ban_id ) ); // phpcs:ignore $result = $wpdb->update( $table, array( 'lifted_at' => current_time( 'mysql', true ) ), array( 'id' => $ban_id ), array( '%s' ), array( '%d' ) ); if ( $ip ) { self::invalidate_cache( $ip ); } return $result; } public static function sweep_expired() { global $wpdb; $table = Argus_DB::table( 'bans' ); $now = current_time( 'mysql', true ); $expired = $wpdb->get_results( $wpdb->prepare( "SELECT id, ip FROM {$table} WHERE lifted_at IS NULL AND expires_at IS NOT NULL AND expires_at <= %s", // phpcs:ignore $now ) ); foreach ( $expired as $row ) { self::lift( $row->id ); Argus_Events::record( 'ban_expired', 'info', sprintf( 'Ban on %s expired and was lifted', $row->ip ), array( 'ip' => $row->ip ), $row->ip ); } return count( $expired ); } public static function recent( $limit = 50 ) { global $wpdb; $table = Argus_DB::table( 'bans' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d", $limit ) ); // phpcs:ignore foreach ( $rows as &$row ) { $row->evidence = json_decode( $row->evidence, true ); } return $rows; } const PER_PAGE = 10; public static function paginated( $page = 1, $per_page = self::PER_PAGE ) { global $wpdb; $table = Argus_DB::table( 'bans' ); $page = max( 1, (int) $page ); $offset = ( $page - 1 ) * $per_page; $now = current_time( 'mysql', true ); $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE lifted_at IS NULL AND (expires_at IS NULL OR expires_at > %s)", $now ) // phpcs:ignore ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE lifted_at IS NULL AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC LIMIT %d OFFSET %d", // phpcs:ignore $now, $per_page, $offset ) ); foreach ( $rows as &$row ) { $row->evidence = json_decode( $row->evidence, true ); } return array( 'rows' => $rows, 'total' => $total, 'total_pages' => max( 1, (int) ceil( $total / $per_page ) ), 'page' => $page, ); } public static function manual_ban( $ip, $reason ) { return self::ban( $ip, self::SOURCE_MANUAL, $reason ?: 'Manually banned by administrator', array(), self::LEVEL_EXTENDED ); } protected static function ttl_for_level( $level ) { switch ( $level ) { case self::LEVEL_TEMPORARY: return 15 * MINUTE_IN_SECONDS; case self::LEVEL_EXTENDED: return DAY_IN_SECONDS; case self::LEVEL_PERMANENT: return 0; default: return 15 * MINUTE_IN_SECONDS; } } protected static function escalate( $current, $incoming ) { $rank = array( self::LEVEL_TEMPORARY => 1, self::LEVEL_EXTENDED => 2, self::LEVEL_PERMANENT => 3 ); return ( $rank[ $incoming ] ?? 1 ) > ( $rank[ $current ] ?? 1 ) ? $incoming : $current; } }