'locked',
'release' => 'released',
'delete' => 'deleted',
];
/**
* Constructor
*/
public function __construct() {
global $wpdb;
$this->tablename = $wpdb->prefix . 'ao_block_logins';
$this->setup_db();
}
/**
* Install new database if necessary
*
* @return void
*/
private function setup_db() {
$db_option = get_option( self::OPTION_NAME, [] );
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
if ( empty( $db_option ) || 1 !== $db_option['db_installed'] || ( 1 === $db_option['db_installed'] && self::DB_VERSION !== $db_option['db_version'] ) ) {
global $wpdb;
Table_Rename_Helper::maybe_rename( $wpdb->prefix . 'blocked_logins', $this->tablename );
$sql = "CREATE TABLE {$this->tablename} (
ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
ip_address varchar(40) NOT NULL,
username varchar(24) NOT NULL,
lockout_count int(10) unsigned NOT NULL,
lockout_time int(10) NOT NULL,
lockout_duration varchar(10) NOT NULL,
release_time int(10) NOT NULL,
status varchar(10) NOT NULL,
PRIMARY KEY (ID),
UNIQUE KEY ip_address (ip_address));";
dbDelta( $sql );
$db_option['db_installed'] = 1;
$db_option['db_version'] = self::DB_VERSION;
update_option( self::OPTION_NAME, $db_option, false );
}
}
/**
* Database function to add blocked record
*
* @param array $postarr list of block records.
*
* @return int
*/
public function insert_record( $postarr ) {
global $wpdb;
$defaults = array(
'ip_address' => '',
'username' => '',
'lockout_count' => 1,
'lockout_time' => time(),
'lockout_duration' => '15',
'status' => 'locked',
);
$postarr = wp_parse_args( $postarr, $defaults );
if ( isset( $postarr['ID'] ) ) {
unset( $postarr['ID'] );
}
$ip_address = $postarr['ip_address'];
$username = sanitize_user( $postarr['username'] );
$lockout_count = absint( $postarr['lockout_count'] );
$lockout_time = absint( $postarr['lockout_time'] );
$lockout_duration = absint( $postarr['lockout_duration'] );
$status = in_array( $postarr['status'], [ 'locked', 'blacklisted', 'whitelisted','released' ], true ) ? $postarr['status'] : 'locked';
$data = compact(
'ip_address',
'username',
'lockout_count',
'lockout_time',
'lockout_duration',
'status'
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $this->tablename, $data );
return $wpdb->insert_id;
}
/**
* Retrieve lockout count from database
*
* @param string $ip_addr IP address.
*
* @return int
*/
public function get_lockout_count( $ip_addr ) {
global $wpdb;
$is_ip_valid = filter_var( $ip_addr, FILTER_VALIDATE_IP );
if ( $is_ip_valid ) {
$cache_key = 'block_login_lockout_count' . $ip_addr;
$count = wp_cache_get( $cache_key, self::CACHE_GROUP );
if ( false === $count ) {
$count = $wpdb->get_var( $wpdb->prepare( "SELECT lockout_count FROM `{$this->tablename}` WHERE ip_address = %s", $ip_addr ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
wp_cache_set( $cache_key, $count, self::CACHE_GROUP );
}
return ( null === $count ) ? 0 : (int) $count;
} else {
return 0;
}
}
/**
* Compute the configured lockout duration for an IP, in seconds. Runs
* the `adminoptim_block_login_lockout_duration` filter (where Pro
* supplies its configured/escalated duration) against the IP's
* existing lockout count -- the single source every lockout path
* (automatic and manual) uses, so neither can drift out of sync.
*
* @param string $ip_addr IP address.
*
* @return int
*/
public function get_lockout_duration( $ip_addr ) {
$lockout_count = $this->get_lockout_count( $ip_addr );
// default is 15 minutes.
return (int) apply_filters( 'adminoptim_block_login_lockout_duration', 15 * 60, $lockout_count );
}
/**
* Update record in database
*
* @param string $ip_addr IP address.
* @param string $username User's username.
* @param string $status Lockout status.
* @param int $duration Lockout duration.
*
* @return void
*/
public function update_record( $ip_addr, $username, $status, int $duration ) {
global $wpdb;
$is_ip_valid = filter_var( $ip_addr, FILTER_VALIDATE_IP );
$username = sanitize_user( $username );
$status = in_array( $status, [ 'locked', 'released' ], true ) ? $status : 'locked';
$lockout_time = time();
if ( $is_ip_valid ) {
$cache_key = 'block_login_have_row' . $ip_addr;
$record = wp_cache_get( $cache_key, self::CACHE_GROUP );
if ( false === $record ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$record = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM `{$this->tablename}` WHERE ip_address = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$ip_addr
)
);
wp_cache_set( $cache_key, $record, self::CACHE_GROUP );
}
if ( null === $record ) {
$this->insert_record(
[
'ip_address' => $ip_addr,
'username' => $username,
'lockout_count' => 1,
'lockout_time' => $lockout_time,
'lockout_duration' => $duration,
'release_time' => $lockout_time + $duration,
'status' => $status,
]
);
} else {
$count = $record->lockout_count + 1;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->update(
$this->tablename,
[
'lockout_count' => $count,
'lockout_time' => $lockout_time,
'lockout_duration' => $duration,
'release_time' => $lockout_time + $duration,
'status' => $status,
],
[ 'ip_address' => $ip_addr ]
);
}
}
}
/**
* Summary of lockouts for the Site Health row, from this module's own table.
*
* The table keeps one row per IP and overwrites `lockout_time` on every
* new lockout, so `recent_ips` is the number of IPs whose *latest*
* lockout fell on/after `$since` -- not a count of lockout events -- and
* `last_lockout` is the newest `lockout_time` overall (0 if none).
*
* @param int $since Unix timestamp the recent window starts at.
*
* @return array{recent_ips:int,last_lockout:int}
*/
public static function get_lockout_summary( int $since ): array {
global $wpdb;
$tablename = $wpdb->prefix . 'ao_block_logins';
// Table name is `$wpdb->prefix` plus a constant, so it is interpolated and prepare() only handles the values (the `%i` identifier placeholder needs WP 6.2+; the plugin supports 5.9).
$row = $wpdb->get_row( $wpdb->prepare( "SELECT COALESCE( SUM( lockout_time >= %d ), 0 ) AS recent_ips, COALESCE( MAX( lockout_time ), 0 ) AS last_lockout FROM `{$tablename}`", $since ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return [
'recent_ips' => $row ? (int) $row->recent_ips : 0,
'last_lockout' => $row ? (int) $row->last_lockout : 0,
];
}
/**
* Get blocked ID
*
* @return array
*/
public static function get_ids() {
global $wpdb;
$tablename = $wpdb->prefix . 'ao_block_logins';
$cache_key = 'block_login_get_ids';
$ids = wp_cache_get( $cache_key, self::CACHE_GROUP );
if ( false === $ids ) {
$ids = $wpdb->get_col( "SELECT ID FROM `{$tablename}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- no user input, nothing to prepare.
wp_cache_set( $cache_key, $ids, self::CACHE_GROUP );
}
return $ids;
}
/**
* Get record by ID
*
* @param int $id Record's ID.
*
* @return array|object|\stdClass|null
*/
public static function get_record_by_id( int $id ) {
global $wpdb;
$tablename = $wpdb->prefix . 'ao_block_logins';
$cache_key = 'block_login_get_record_' . $id;
$record = wp_cache_get( $cache_key, self::CACHE_GROUP );
if ( false === $record ) {
$record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$tablename}` WHERE ID = %d", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
wp_cache_set( $cache_key, $record, self::CACHE_GROUP );
}
return $record;
}
/**
* Delete record from db
*
* @param int $id Record's ID.
*
* @return bool|int|\mysqli_result|null
*/
public function delete_record( int $id ) {
global $wpdb;
wp_cache_delete( 'block_login_get_ids', self::CACHE_GROUP );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
return $wpdb->delete( $this->tablename, [ 'ID' => $id ] );
}
/**
* Manage IP address blocked status
*
* @param int $ip_id Record ID.
* @param string $status Record status.
*
* @return array|\WP_Error
*/
public function manage_ip_status( int $ip_id, $status ) {
global $wpdb;
$record = self::get_record_by_id( $ip_id );
$ip_addr = $record->ip_address;
$ip_number = Block_Login::ip_to_number( $ip_addr );
switch ( $status ) {
case 'delete':
delete_transient( 'adminoptim_locked_' . $ip_number );
delete_transient( 'adminoptim_failed_login_' . $ip_number );
$result = $this->delete_record( $ip_id );
if ( $result > 0 ) {
$response['message'] = ao_get_admin_notice(
__( 'IP log deleted successfully.', 'admin-optimizer' ),
[
'type' => 'success',
'dismissible' => true,
]
);
} else {
$response = new \WP_Error(
'adminoptim-error-delete-ip',
ao_get_admin_notice(
__( 'Error: IP log not deleted.', 'admin-optimizer' ),
[
'type' => 'error',
'dismissible' => true,
]
)
);
}
break;
case 'release':
delete_transient( 'adminoptim_locked_' . $ip_number );
delete_transient( 'adminoptim_failed_login_' . $ip_number );
$release_time = time();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$result = $wpdb->update(
$this->tablename,
[
'status' => 'released',
'release_time' => $release_time,
],
[
'ID'
=> $ip_id,
]
);
if ( $result > 0 ) {
wp_cache_delete( 'block_login_get_record_' . $ip_id, self::CACHE_GROUP );
$response['message'] = ao_get_admin_notice(
__( 'IP lockout released successfully.', 'admin-optimizer' ),
[
'type' => 'success',
'dismissible' => true,
]
);
$response['release_time'] = wp_date( 'Y-m-d H:i:s', $release_time );
$response['status'] = __( 'Released', 'admin-optimizer' );
} else {
$response = new \WP_Error(
'adminoptim-error-release-ip',
ao_get_admin_notice(
__( 'Error: IP release not successful.', 'admin-optimizer' ),
[
'type' => 'error',
'dismissible' => true,
]
)
);
}
break;
case 'lock':
default:
$record = self::get_record_by_id( $ip_id );
$ip_addr = $record->ip_address;
$ip_number = Block_Login::ip_to_number( $ip_addr );
$lockout_duration = $this->get_lockout_duration( $ip_addr );
set_transient( 'adminoptim_locked_' . $ip_number, true, $lockout_duration );
$lockout_time = time();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$result = $wpdb->update(
$this->tablename,
[
'status' => 'locked',
'lockout_time' => $lockout_time,
'lockout_duration' => $lockout_duration,
'release_time' => $lockout_time + $lockout_duration,
],
[ 'ID' => $ip_id ]
);
if ( $result > 0 ) {
wp_cache_delete( 'block_login_get_record_' . $ip_id, self::CACHE_GROUP );
$response['message'] = ao_get_admin_notice(
__( 'IP locked successfully.', 'admin-optimizer' ),
[
'type' => 'success',
'dismissible' => true,
]
);
$response['lockout_time'] = wp_date( 'Y-m-d H:i:s', $lockout_time );
$response['lockout_duration'] = human_time_diff( 1, $lockout_duration );
$response['release_time'] = wp_date( 'Y-m-d H:i:s', $lockout_time + $lockout_duration );
$response['status'] = __( 'Locked', 'admin-optimizer' );
} else {
$response = new \WP_Error(
'adminoptim-error-lockining-ip',
ao_get_admin_notice(
__( 'Error: IP record cannot be locked.', 'admin-optimizer' ),
[
'type' => 'error',
'dismissible' => true,
]
)
);
}
break;
}
return $response;
}
/**
* Clean lockout
*
* @return void
*/
public function clean_lockout() {
global $wpdb;
$current_time = time();
$records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$this->tablename}` WHERE release_time < %d AND status = 'locked'", $current_time ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( null !== $records ) {
foreach ( $records as $record ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->update( $this->tablename, [ 'status' => 'released' ], [ 'ID' => $record->ID ] );
wp_cache_delete( 'block_login_get_record_' . $record->ID, self::CACHE_GROUP );
}
}
}
}