options = get_option(
self::OPTION_NAME,
[
'failed_count' => 3,
]
);
$this->init();
}
/**
* Start the engine running
*
* @return void
*/
public function init() {
$this->db = new Block_Login_DB();
$this->settings = new Block_Login_Settings();
add_action( 'adminoptimizer_add_submenu_page', [ $this, 'add_submenu_page' ] );
add_action( 'wp_login_failed', [ $this, 'update_failed_login_record' ] );
add_filter( 'wp_authenticate_user', [ $this, 'maybe_allow_login' ], 9999 );
add_filter( 'shake_error_codes', [ $this, 'add_failed_login_error_code' ] );
add_filter( 'login_errors', [ $this, 'show_login_error_messages' ] );
add_action( 'wp_ajax_adminoptim_login_action', [ $this, 'ajax_settings_login_action' ] );
add_filter( 'login_message', [ $this, 'show_lockout_message' ] );
}
/**
* Add submenu to WP Menu page
*
* @return void
*/
public function add_submenu_page() {
add_submenu_page( ADMINOPTIMIZER_MODULES_MENU_SLUG, __( 'Block Failed Login', 'admin-optimizer' ), __( 'Block Failed Login', 'admin-optimizer' ), 'manage_options', self::MENU_SLUG, [ $this->settings, 'render_settings_page' ] );
}
/**
* Handle failed login
*
* @param string $username user's username.
*
* @return void
*/
public function update_failed_login_record( $username ) {
$ip_addr = $this->get_ip_address();
if ( ! $ip_addr ) {
// can't retrieve IP address, return.
return;
} else {
$ip_number = self::ip_to_number( $ip_addr );
$lockout = get_transient( 'adminoptim_locked_' . $ip_number );
if ( ! $lockout ) {
$failed_login = get_transient( 'adminoptim_failed_login_' . $ip_number );
if ( false === $failed_login ) {
// 1st failed attempt. Add a new transient value
set_transient( 'adminoptim_failed_login_' . $ip_number, 1, 0.5 * DAY_IN_SECONDS );
} else {
$failed_login = (int) $failed_login;
++$failed_login;
if ( $failed_login >= (int) $this->options['failed_count'] ) {
$lockout_count = $this->db->get_lockout_count( $ip_addr );
// start lockdown for 15 minutes.
$lockout_duration = apply_filters( 'adminoptim_block_login_lockout_duration', 15 * 60, $lockout_count ); // default is 15 minutes.
set_transient( 'adminoptim_locked_' . $ip_number, true, $lockout_duration );
$this->db->update_record( $ip_addr, $username, 'locked', $lockout_duration );
delete_transient( 'adminoptim_failed_login_' . $ip_number );
} else {
set_transient( 'adminoptim_failed_login_' . $ip_number, $failed_login, 0.5 * DAY_IN_SECONDS );
}
}
}
}
}
/**
* Get current user's IP address
*
* @return false|string
*/
public function get_ip_address() {
$remote_addr = '';
if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
$remote_addr = trim( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) );
}
if ( $remote_addr && filter_var( $remote_addr, FILTER_VALIDATE_IP ) ) {
$trusted_proxy = apply_filters( 'adminoptim_block_login_trust_proxy_headers', false, $remote_addr );
if ( ! $trusted_proxy ) {
return $remote_addr;
}
}
$header_keys = [ 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED' ];
foreach ( $header_keys as $header_key ) {
if ( empty( $_SERVER[ $header_key ] ) ) {
continue;
}
$header_value = trim( sanitize_text_field( wp_unslash( $_SERVER[ $header_key ] ) ) );
if ( '' === $header_value ) {
continue;
}
if ( 'HTTP_FORWARDED' === $header_key && preg_match( '/for=(?:"?\\[?)([^\\]";,\\s]+)(?:\\]?"?)/i', $header_value, $matches ) ) {
$ip_candidate = trim( $matches[1] );
} else {
$ip_candidate = trim( explode( ',', $header_value )[0] );
}
if ( filter_var( $ip_candidate, FILTER_VALIDATE_IP ) ) {
return $ip_candidate;
}
}
if ( $remote_addr && filter_var( $remote_addr, FILTER_VALIDATE_IP ) ) {
return $remote_addr;
}
return false;
}
/**
* Convert IP address to a storable string
*
* @param string $ip IP address.
*
* @return false|int|string
*/
public static function ip_to_number( $ip ) {
// for ipv4 address only.
$long = ip2long( $ip );
if ( false !== $long ) {
return $long;
} else {
// long is false, so ip is ipv6, try another method.
$bin = inet_pton( $ip );
if ( $bin ) {
return bin2hex( $bin );
} else {
// invalid ip address.
return false;
}
}
}
/**
* Function to block user login after too many unsuccessful login attempts
*
* @param \WP_User $user WP_User class.
*
* @return \WP_User|\WP_Error
*/
public function maybe_allow_login( $user ) {
if ( is_wp_error( $user ) ) {
return $user;
}
$is_lockout = $this->is_lockout();
if ( $is_lockout ) {
$ip_number = self::ip_to_number( $this->get_ip_address() );
$lockout_time = (int) get_option( '_transient_timeout_adminoptim_locked_' . $ip_number, 0 );
$readable_time = human_time_diff( time(), $lockout_time );
return new \WP_Error(
'too_many_failed_logins',
/* translators: %s: blocked time interval */
sprintf( __( 'ERROR: Too many failed login attempts. Login blocked for %s. Please try again later.', 'admin-optimizer' ), $readable_time )
);
} else {
$this->db->clean_lockout();
}
return $user;
}
/**
* Check if an IP address is being locked out
*
* @return bool
*/
public function is_lockout() {
$is_lockout = false;
$ip_addr = $this->get_ip_address();
if ( $ip_addr ) {
$ip_number = self::ip_to_number( $ip_addr );
$lockout = get_transient( 'adminoptim_locked_' . $ip_number );
if ( $lockout ) {
$is_lockout = true;
}
}
return $is_lockout;
}
/**
* Check for valid login
*
* @return true|\WP_Error
*/
public function check_login_valid() {
$ip_addr = $this->get_ip_address();
if ( ! $ip_addr ) {
// can't retrieve IP address, return.
return true;
}
$ip_number = self::ip_to_number( $ip_addr );
$lockout = get_transient( 'adminoptim_locked_' . $ip_number );
$failed_login = get_transient( 'adminoptim_failed_login_' . $ip_number );
if ( $lockout ) {
$lockout_time = (int) get_option( '_transient_timeout_adminoptim_locked_' . $ip_number, 0 );
$readable_time = human_time_diff( time(), $lockout_time );
return new \WP_Error(
'too_many_failed_logins',
/* translators: %s: blocked time interval */
sprintf( __( 'ERROR: Too many failed login attempts. Login blocked for %s. Please try again later.', 'admin-optimizer' ), $readable_time )
);
} elseif ( is_numeric( $failed_login ) && $failed_login > 0 ) {
$remaining_attempts = (int) $this->options['failed_count'] - (int) $failed_login;
if ( $remaining_attempts > 0 ) {
return new \WP_Error(
'failed_login_count',
/* translators: %s: number of login attempts left */
sprintf( __( 'ERROR: %s login attempts left.', 'admin-optimizer' ), $remaining_attempts )
);
} else {
return new \WP_Error(
'too_many_failed_logins',
__(
'ERROR: Too many failed login attempts. Login blocked for 15 minutes. Please try again later.',
'admin-optimizer'
)
);
}
}
return true;
}
/**
* Append to failed login error codes
*
* @param array $codes List of failed login error codes.
*
* @return array
*/
public function add_failed_login_error_code( $codes ) {
$codes[] = 'failed_login_count';
$codes[] = 'too_many_failed_logins';
return $codes;
}
/**
* Function to check if need to show login error message
*
* @return bool
*/
private function should_show_login_error_msg() {
if ( isset( $_GET['key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
/* reset password */
return false;
}
$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action_arr = [ 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register' ];
return ! in_array( $action, $action_arr, true );
}
/**
* Function to show login error message
*
* @param string $error error message.
*
* @return string
*/
public function show_login_error_messages( $error ) {
if ( ! $this->should_show_login_error_msg() ) {
return $error;
}
$login_error = $this->check_login_valid();
if ( is_wp_error( $login_error ) ) {
return $login_error->get_error_message();
}
return $error;
}
/**
* Ajax action to manage blocked logins
*
* @return void
*/
public function ajax_settings_login_action() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
new \WP_Error(
'adminoptim-unauthorized-user',
ao_get_admin_notice(
__( 'Unauthorized action.', 'admin-optimizer' ),
[
'type' => 'error',
]
)
)
);
}
check_ajax_referer( 'adminoptim_block_login_action', '_wpnonce' );
$response = [];
$ip_id = ! empty( $_REQUEST['ip_id'] ) ? (int) $_REQUEST['ip_id'] : 0;
$ip_action = ! empty( $_REQUEST['ip_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ip_action'] ) ) : '';
if ( array_key_exists( $ip_action, $this->db->all_statuses ) ) {
$response = $this->db->manage_ip_status( $ip_id, $ip_action );
} else {
$error = new \WP_Error(
'adminoptim-invalid-action',
ao_get_admin_notice(
__( 'Invalid action', 'admin-optimizer' ),
[
'type' => 'error',
'dismissible' => true,
]
)
);
wp_send_json_error( $error );
}
if ( is_wp_error( $response ) ) {
wp_send_json_error( $response );
} else {
wp_send_json_success( $response );
}
}
/**
* Function to show lockout message
*
* @param string $message lockout message.
*
* @return string
*/
public function show_lockout_message( $message ) {
if ( $this->is_lockout() ) {
$message = ao_get_admin_notice(
__( 'ERROR: Too many failed login attempts. Please try again later.', 'admin-optimizer' ),
[
'type' => 'error',
'additional_classes' => [ 'message' ],
]
);
}
return $message;
}
}