Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user last seen information #4765

Open
wants to merge 47 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
006ac0e
Add user last seen information
luismulinari Aug 9, 2023
5fadcf7
Linting fixes
luismulinari Aug 9, 2023
e122725
Code review suggestions
luismulinari Aug 14, 2023
6de9dd9
Lockdown inactive users
luismulinari Aug 15, 2023
141bb9a
Merge branch 'develop' into add/users-last-seen
luismulinari Aug 15, 2023
e920989
Fix priority
luismulinari Aug 15, 2023
5a4a93d
Fix filter
luismulinari Aug 15, 2023
2b5f8f6
Simplify code execution flow
luismulinari Aug 15, 2023
91fcbe1
Apply suggestions from code review
luismulinari Aug 18, 2023
dd56cdd
Apply suggestions from code review
luismulinari Aug 18, 2023
bf897f2
Merge branch 'develop' into add/users-last-seen
luismulinari Aug 18, 2023
d8ba237
Add last seen to network users list
luismulinari Aug 21, 2023
8cf88fd
Move the logic to a class
luismulinari Aug 21, 2023
df19032
Merge branch 'develop' into add/users-last-seen
luismulinari Aug 21, 2023
267e26f
Add unit tests
luismulinari Aug 21, 2023
26bdd27
Linting fixes
luismulinari Aug 21, 2023
d1bdbb4
Fix tests to run on WP 5.8
luismulinari Aug 22, 2023
eef4ca2
Merge branch 'develop' into add/users-last-seen
luismulinari Aug 22, 2023
5287197
Update test-user-last-seen.php
luismulinari Aug 22, 2023
ccdb6b4
Merge branch 'develop' into add/users-last-seen
luismulinari Aug 22, 2023
bb49b0c
Code review suggestions
luismulinari Aug 24, 2023
c74c4bb
Check user caps inside is_considered_inactive
luismulinari Aug 24, 2023
c96617c
Merge branch 'develop' into add/users-last-seen
luismulinari Aug 30, 2023
30394d5
Merge branch 'develop' into add/users-last-seen
luismulinari Sep 1, 2023
82ad874
Merge branch 'develop' into add/users-last-seen
luismulinari Sep 5, 2023
d5adcb4
Couple tweaks
WPprodigy Sep 6, 2023
f7ef136
Introduce ignore inactivity check until
luismulinari Sep 6, 2023
d902171
Add extra time when user is promoted to prevent user lockdown
luismulinari Sep 7, 2023
33ace77
Merge branch 'develop' into add/users-last-seen
luismulinari Sep 14, 2023
ed87a3e
Add tests; Fix linting
Sep 14, 2023
9cb737b
Fix linting
luismulinari Sep 14, 2023
5e37e4b
Merge branch 'develop' into add/users-last-seen
luismulinari Sep 25, 2023
3dc2715
Avoid using determine_user filter
luismulinari Sep 25, 2023
5e2d1b1
Merge branch 'develop' into add/users-last-seen
luismulinari Sep 25, 2023
550b4c4
Register activity on set_current_user and rest_authentication_errors
luismulinari Sep 27, 2023
d622840
Merge branch 'develop' into add/users-last-seen
luismulinari Oct 2, 2023
a4275c9
Merge branch 'develop' into add/users-last-seen
WPprodigy Oct 17, 2023
24b1f82
Use the determine_current_user filter to record activity; Use the wp_…
luismulinari Oct 18, 2023
182dc0d
Use a class property rather than a global
luismulinari Oct 19, 2023
6831bd7
Merge branch 'develop' into add/users-last-seen
luismulinari Oct 19, 2023
53c9bbb
Merge branch 'develop' into add/users-last-seen
luismulinari Oct 20, 2023
bebe719
Merge branch 'develop' into add/users-last-seen
luismulinari Jan 3, 2024
b70f9a2
Merge branch 'develop' into add/users-last-seen
rinatkhaziev Jan 9, 2024
617ff07
Merge branch 'develop' into add/users-last-seen
luismulinari Jan 25, 2024
37eb090
Check if constant is defined before using it
luismulinari Jan 25, 2024
dff4503
Merge branch 'develop' into add/users-last-seen
luismulinari Mar 26, 2024
0bad466
Merge branch 'develop' into add/users-last-seen
luismulinari Jun 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions security.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
require_once __DIR__ . '/security/login-error.php';
require_once __DIR__ . '/security/password.php';

if ( defined( 'VIP_USER_LAST_SEEN_ENABLED' ) ) {
require_once __DIR__ . '/security/user-last-seen.php';
}

use Automattic\VIP\Utils\Context;

define( 'CACHE_GROUP_LOGIN_LIMIT', 'vip_login_limit' );
Expand Down
148 changes: 148 additions & 0 deletions security/user-last-seen.php
mjangda marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php
namespace Automattic\VIP\Security;

const LAST_SEEN_META_KEY = 'wpvip_last_seen';
const LAST_SEEN_UPDATE_USER_META_CACHE_TTL = MINUTE_IN_SECONDS * 5; // Store last seen once every five minute to avoid too many write DB operations
const LAST_SEEN_UPDATE_USER_META_CACHE_KEY_PREFIX = 'wpvip_last_seen_update_user_meta_cache_key';
const GROUP = 'wpvip';
WPprodigy marked this conversation as resolved.
Show resolved Hide resolved

add_filter( 'determine_current_user', function ( $user_id ) {
if ( ! $user_id ) {
return $user_id;
}

$cache_key = LAST_SEEN_UPDATE_USER_META_CACHE_KEY_PREFIX . $user_id;

if ( wp_cache_get( $cache_key, GROUP ) ) {
// Last seen meta was checked recently
return $user_id;
}

$is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );
luismulinari marked this conversation as resolved.
Show resolved Hide resolved

if ( $is_api_request && is_considered_inactive( $user_id ) ) {
// To block API requests for inactive requests, we need to return a WP_Error object here
return new \WP_Error( 'inactive_account', __( '<strong>Error</strong>: Your account has been flagged as inactive. Please contact your site administrator.', 'inactive-account-lockdown' ) );
}
WPprodigy marked this conversation as resolved.
Show resolved Hide resolved

if ( wp_cache_add( $cache_key, true, GROUP, LAST_SEEN_UPDATE_USER_META_CACHE_TTL ) ) {
update_user_meta( $user_id, LAST_SEEN_META_KEY, time() );
}

return $user_id;
}, 30, 1 );

add_filter( 'authenticate', function( $user, string $username, string $password ) {
if ( is_wp_error( $user ) ) {
return $user;
}

if ( $user->ID && is_considered_inactive( $user->ID ) ) {
return new \WP_Error( 'inactive_account', __( '<strong>Error</strong>: Your account has been flagged as inactive. Please contact your site administrator.', 'inactive-account-lockdown' ) );;
}

return $user;
}, 20, 3 );


add_filter( 'manage_users_columns', function ( $cols ) {
$cols['last_seen'] = __( 'Last seen' );
return $cols;
} );

add_filter( 'manage_users_custom_column', function ( $default, $column_name, $user_id ) {
if ( 'last_seen' !== $column_name ) {
return $default;
}

$last_seen_timestamp = get_user_meta( $user_id, LAST_SEEN_META_KEY, true );

if ( ! $last_seen_timestamp ) {
return $default;
}

$formatted_date = sprintf(
__( '%1$s at %2$s' ),
date_i18n( get_option('date_format'), $last_seen_timestamp ),
date_i18n( get_option('time_format'), $last_seen_timestamp )
);

if ( ! is_considered_inactive( $user_id ) ) {
return sprintf( '<span>%s</span>', esc_html__( $formatted_date ) );
}

$unblock_link = '';
if ( current_user_can( 'edit_user', array() ) ) {
luismulinari marked this conversation as resolved.
Show resolved Hide resolved
$url = add_query_arg( array(
'action' => 'reset_last_seen',
'user_id' => $user_id,
) );

$unblock_link = "<div class='row-actions'><span>User blocked due to inactivity. <a class='reset_last_seen_action' href='" . esc_url( $url ) . "'>" . __( 'Unblock' ) . "</a></span></div>";
}
return sprintf( '<span class="wp-ui-text-notification">%s</span>' . $unblock_link, esc_html__( $formatted_date ) );
}, 10, 3 );

add_action( 'user_row_actions', function ( $actions ) {
if( isset($_GET['action'] ) && $_GET['action'] === 'reset_last_seen' ){
$user_id = $_GET['user_id'];
luismulinari marked this conversation as resolved.
Show resolved Hide resolved
delete_user_meta( $user_id, LAST_SEEN_META_KEY );
WPprodigy marked this conversation as resolved.
Show resolved Hide resolved
}

return $actions;
}, 10, 1 );

add_filter( 'views_users', function ( $views ) {
global $wpdb;

if ( ! defined( 'VIP_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) {
return $views;
}

$count = $wpdb->get_var( 'SELECT COUNT(meta_key) FROM ' . $wpdb->usermeta . ' WHERE meta_key = "' . LAST_SEEN_META_KEY . '" AND meta_value < ' . get_inactivity_timestamp() );
luismulinari marked this conversation as resolved.
Show resolved Hide resolved

$view = __( 'Blocked Users' );
if ( $count ) {
$class = isset( $_REQUEST[ 'last_seen_filter' ] ) ? 'current' : '';
$view = '<a class="' . $class . '" href="users.php?last_seen_filter=blocked">' . $view . '</a>';
luismulinari marked this conversation as resolved.
Show resolved Hide resolved
}
$views['blocked_users'] = $view . ' (' . $count . ')';

return $views;
} );

add_filter( 'users_list_table_query_args', function ( $args ) {
if ( ! defined( 'VIP_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) {
return $args;
}

if ( isset( $_REQUEST[ 'last_seen_filter' ] ) ) {
$args[ 'meta_key' ] = LAST_SEEN_META_KEY;
$args[ 'meta_value' ] = get_inactivity_timestamp();
$args[ 'meta_type' ] = 'NUMERIC';
$args[ 'meta_compare' ] = '<';
}

return $args;
} );

function is_considered_inactive( $user_id ) {
if ( ! defined( 'VIP_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) {
return false;
}

$last_seen_timestamp = get_user_meta( $user_id, LAST_SEEN_META_KEY, true );
if ( ! $last_seen_timestamp ) {
return false;
}
WPprodigy marked this conversation as resolved.
Show resolved Hide resolved

return $last_seen_timestamp < get_inactivity_timestamp();
}

function get_inactivity_timestamp() {
if ( ! defined( 'VIP_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) {
return 0;
}

return strtotime( sprintf('-%d days', constant( 'VIP_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) ) + LAST_SEEN_UPDATE_USER_META_CACHE_TTL;
}