diff --git a/security/user-last-seen.php b/security/user-last-seen.php
index 4b8151ccc8..0a240d507f 100644
--- a/security/user-last-seen.php
+++ b/security/user-last-seen.php
@@ -8,15 +8,17 @@ class User_Last_Seen {
const LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY = 'wpvip_last_seen_release_date_timestamp';
public function init() {
- if ( ! defined( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) || constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'NO_ACTION' ) {
+ if ( ! defined( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) || constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'NO_ACTION' ) {
return;
}
// Use a global cache group to avoid having to the data for each site
wp_cache_add_global_groups( array( self::LAST_SEEN_CACHE_GROUP ) );
+ add_action( 'admin_init', array( $this, 'register_release_date' ) );
+
add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 30, 1 );
- add_filter( 'authenticate', array( $this, 'determine_current_user' ), 20, 1 );
+ add_filter( 'authenticate', array( $this, 'authenticate' ), 20, 1 );
if ( in_array( constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ), array( 'REPORT', 'BLOCK' ) ) ) {
add_filter( 'wpmu_users_columns', array( $this, 'add_last_seen_column_head' ) );
@@ -25,17 +27,16 @@ public function init() {
add_filter( 'manage_users_sortable_columns', array( $this, 'add_last_seen_sortable_column' ) );
add_filter( 'manage_users-network_sortable_columns', array( $this, 'add_last_seen_sortable_column' ) );
- add_filter( 'users_list_table_query_args', array( $this, 'last_seen_query_args') );
+ add_filter( 'users_list_table_query_args', array( $this, 'last_seen_order_by_query_args') );
+ }
+ if ( $this->is_block_action_enabled() ) {
add_filter( 'views_users', array( $this, 'add_blocked_users_filter' ) );
add_filter( 'views_users-network', array( $this, 'add_blocked_users_filter' ) );
- }
+ add_filter( 'users_list_table_query_args', array( $this, 'last_seen_blocked_users_filter_query_args') );
- if ( constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'BLOCK' ) {
add_action( 'admin_init', array( $this, 'last_seen_unblock_action' ) );
}
-
- add_action( 'admin_init', array( $this, 'register_release_date' ) );
}
public function determine_current_user( $user_id ) {
@@ -48,7 +49,7 @@ public function determine_current_user( $user_id ) {
return $user_id;
}
- if ( $this->is_considered_inactive( $user_id ) ) {
+ if ( $this->is_block_action_enabled() && $this->is_considered_inactive( $user_id ) ) {
// Force current user to 0 to avoid recursive calls to this filter
wp_set_current_user( 0 );
@@ -67,30 +68,34 @@ public function authenticate( $user ) {
return $user;
}
- if ( $user->ID && $this->is_considered_inactive( $user->ID ) ) {
+ if ( $user->ID && $this->is_block_action_enabled() && $this->is_considered_inactive( $user->ID ) ) {
return new \WP_Error( 'inactive_account', __( 'Error: Your account has been flagged as inactive. Please contact your site administrator.', 'inactive-account-lockdown' ) );;
}
return $user;
}
- function add_last_seen_column_head( $columns ) {
+ public function add_last_seen_column_head( $columns ) {
$columns[ 'last_seen' ] = __( 'Last seen' );
return $columns;
}
- function add_last_seen_sortable_column( $columns ) {
+ public function add_last_seen_sortable_column( $columns ) {
$columns['last_seen'] = 'last_seen';
return $columns;
}
- function last_seen_query_args( $vars ) {
+ public function last_seen_order_by_query_args( $vars ) {
if ( isset( $vars['orderby'] ) && $vars['orderby'] === 'last_seen' ) {
$vars[ 'meta_key' ] = self::LAST_SEEN_META_KEY;
$vars[ 'orderby' ] = 'meta_value_num';
}
+ return $vars;
+ }
+
+ public function last_seen_blocked_users_filter_query_args($vars ) {
if ( isset( $_REQUEST[ 'last_seen_filter' ] ) && $_REQUEST[ 'last_seen_filter' ] === 'blocked' ) {
$vars[ 'meta_key' ] = self::LAST_SEEN_META_KEY;
$vars[ 'meta_value' ] = $this->get_inactivity_timestamp();
@@ -101,7 +106,7 @@ function last_seen_query_args( $vars ) {
return $vars;
}
- function add_last_seen_column_date( $default, $column_name, $user_id ) {
+ public function add_last_seen_column_date( $default, $column_name, $user_id ) {
if ( 'last_seen' !== $column_name ) {
return $default;
}
@@ -118,7 +123,7 @@ function add_last_seen_column_date( $default, $column_name, $user_id ) {
date_i18n( get_option('time_format'), $last_seen_timestamp )
);
- if ( ! $this->is_considered_inactive( $user_id ) ) {
+ if ( ! $this->is_block_action_enabled() || ! $this->is_considered_inactive( $user_id ) ) {
return sprintf( '%s', esc_html__( $formatted_date ) );
}
@@ -135,7 +140,7 @@ function add_last_seen_column_date( $default, $column_name, $user_id ) {
return sprintf( '%s' . $unblock_link, esc_html__( $formatted_date ) );
}
- function add_blocked_users_filter( $views ) {
+ public function add_blocked_users_filter( $views ) {
$blog_id = is_network_admin() ? null : get_current_blog_id();
$users_query = new \WP_User_Query(
@@ -166,7 +171,7 @@ function add_blocked_users_filter( $views ) {
return $views;
}
- function last_seen_unblock_action() {
+ public function last_seen_unblock_action() {
$admin_notices_hook_name = is_network_admin() ? 'network_admin_notices' : 'admin_notices';
if ( isset( $_GET['reset_last_seen_success'] ) && $_GET['reset_last_seen_success'] === '1' ) {
@@ -233,10 +238,6 @@ public function register_release_date() {
}
public function is_considered_inactive( $user_id ) {
- if ( constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) !== 'BLOCK' ) {
- return false;
- }
-
$last_seen_timestamp = get_user_meta( $user_id, self::LAST_SEEN_META_KEY, true );
if ( $last_seen_timestamp ) {
return $last_seen_timestamp < $this->get_inactivity_timestamp();
@@ -252,10 +253,12 @@ public function is_considered_inactive( $user_id ) {
}
public function get_inactivity_timestamp() {
- if ( ! defined( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) {
- return 0;
- }
-
return strtotime( sprintf('-%d days', constant( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) ) + self::LAST_SEEN_UPDATE_USER_META_CACHE_TTL;
}
+
+ private function is_block_action_enabled() {
+ return defined( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) &&
+ defined( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) &&
+ constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'BLOCK';
+ }
}
diff --git a/tests/security/test-user-last-seen.php b/tests/security/test-user-last-seen.php
new file mode 100644
index 0000000000..8185b99d5f
--- /dev/null
+++ b/tests/security/test-user-last-seen.php
@@ -0,0 +1,219 @@
+init();
+
+ $this->assertFalse( has_filter( 'determine_current_user' ) );
+ $this->assertFalse( has_filter( 'authenticate' ) );
+ }
+
+ public function test__should_not_load_actions_and_filters_when_env_vars_is_set_to_no_action() {
+ Constant_Mocker::define( 'VIP_SECURITY_INACTIVE_USERS_ACTION', 'NO_ACTION' );
+
+ remove_all_filters( 'determine_current_user' );
+ remove_all_filters( 'authenticate' );
+
+ $last_seen = new User_Last_Seen();
+ $last_seen->init();
+
+ $this->assertFalse( has_filter( 'determine_current_user' ) );
+ $this->assertFalse( has_filter( 'authenticate' ) );
+ }
+
+ public function test__is_considered_inactive__should_consider_user_meta()
+ {
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 30);
+
+ $user_inactive_id = $this->factory()->user->create([
+ 'meta_input' => [
+ 'wpvip_last_seen' => strtotime('-31 days'),
+ ],
+ ]);
+
+ $user_active_id = $this->factory()->user->create([
+ 'meta_input' => [
+ 'wpvip_last_seen' => strtotime('-29 days'),
+ ],
+ ]);
+
+ $last_seen = new User_Last_Seen();
+ $last_seen->init();
+
+ $this->assertTrue( $last_seen->is_considered_inactive( $user_inactive_id ) );
+ $this->assertFalse( $last_seen->is_considered_inactive( $user_active_id ) );
+ }
+
+ public function test__is_considered_inactive__should_return_false_if_user_meta_and_option_are_not_present()
+ {
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 30);
+
+ delete_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY );
+
+ $user_without_meta = $this->factory()->user->create();
+
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->init();
+
+ $this->assertFalse( $last_seen->is_considered_inactive( $user_without_meta ) );
+ }
+
+ public function test__is_considered_inactive__should_use_release_date_option_when_user_meta_is_not_defined()
+ {
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15);
+
+ add_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY, strtotime('-16 days') );
+
+ $user_without_meta = $this->factory()->user->create();
+
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->init();
+
+ $this->assertTrue( $last_seen->is_considered_inactive( $user_without_meta ) );
+
+ update_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY, strtotime('-10 days') );
+
+ $this->assertFalse( $last_seen->is_considered_inactive( $user_without_meta ) );
+ }
+
+ public function test__determine_current_user_should_record_once_last_seen_meta()
+ {
+ Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' );
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15);
+
+ remove_all_filters( 'determine_current_user' );
+
+ $previous_last_seen = sprintf('%d', strtotime('-10 days') );
+
+ $user_id = $this->factory()->user->create([
+ 'meta_input' => [
+ 'wpvip_last_seen' => $previous_last_seen,
+ ],
+ ]);
+
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->init();
+
+ apply_filters( 'determine_current_user', $user_id, $user_id );
+
+ $current_last_seen = get_user_meta( $user_id, User_Last_Seen::LAST_SEEN_META_KEY, true );
+
+ $new_user_id = apply_filters( 'determine_current_user', $user_id, $user_id );
+
+ $cached_last_seen = get_user_meta( $user_id, User_Last_Seen::LAST_SEEN_META_KEY, true );
+
+ $this->assertTrue( $current_last_seen > $previous_last_seen );
+ $this->assertSame( $current_last_seen, $cached_last_seen );
+ $this->assertSame( $new_user_id, $user_id );
+ }
+
+ public function test__determine_current_user_should_return_an_error_when_user_is_inactive()
+ {
+ Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' );
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15);
+
+ remove_all_filters( 'determine_current_user' );
+
+ $user_id = $this->factory()->user->create([
+ 'meta_input' => [
+ 'wpvip_last_seen' => strtotime('-100 days'),
+ ],
+ ]);
+
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->init();
+
+ $user = apply_filters( 'determine_current_user', $user_id, $user_id );
+
+ $this->assertWPError( $user, 'Expected WP_Error object to be returned' );
+ }
+
+ public function test__authenticate_should_not_return_error_when_user_is_active()
+ {
+ Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' );
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15);
+
+ remove_all_filters( 'authenticate' );
+
+ $user_id = $this->factory()->user->create([
+ 'meta_input' => [
+ 'wpvip_last_seen' => strtotime('-5 days'),
+ ],
+ ]);
+
+ $user = get_user_by( 'id', $user_id );
+
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->init();
+
+ $new_user = apply_filters( 'authenticate', $user, $user );
+
+ $this->assertSame( $user->ID, $new_user->ID );
+ }
+
+ public function test__authenticate_should_return_an_error_when_user_is_inactive()
+ {
+ Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' );
+ Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15);
+
+ remove_all_filters( 'authenticate' );
+
+ $user_id = $this->factory()->user->create([
+ 'meta_input' => [
+ 'wpvip_last_seen' => strtotime('-100 days'),
+ ],
+ ]);
+ $user = get_user_by( 'id', $user_id );
+
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->init();
+
+ $user = apply_filters( 'authenticate', $user, $user );
+
+ $this->assertWPError( $user, 'Expected WP_Error object to be returned' );
+ }
+
+ public function test__register_release_date_should_register_release_date_only_once()
+ {
+ Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'RECORD_LAST_SEEN' );
+
+ remove_all_actions( 'admin_init' );
+ delete_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY );
+ $last_seen = new \Automattic\VIP\Security\User_Last_Seen();
+ $last_seen->register_release_date();
+
+ $release_date = get_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY );
+
+ $last_seen->register_release_date();
+
+ $new_release_date = get_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY );
+
+ $this->assertIsNumeric( $release_date );
+ $this->assertSame( $release_date, $new_release_date );
+ }
+}