prefix; values are integer-cast or passed through $wpdb->prepare(). Caching would defeat live session tracking. /** * Records sessions on login/logout and wires the front-end heartbeat. * * Normal page views do no database work here: activity is written by the * heartbeat (REST route ndsg/v1/ping; Session Guard Pro swaps in a lighter * endpoint through the ndsg_heartbeat_url filter). */ class Tracker { private $kicked_notice = false; public function register() { add_action( 'set_logged_in_cookie', [ $this, 'on_login' ], 10, 6 ); add_action( 'wp_logout', [ $this, 'on_logout' ] ); add_action( 'after_password_reset', [ $this, 'on_password_reset' ] ); add_action( 'deleted_user', [ $this, 'on_user_deleted' ] ); add_action( 'init', [ $this, 'detect_kicked_cookie' ], 20 ); add_action( 'wp_footer', [ $this, 'print_kicked_notice' ] ); add_filter( 'login_message', [ $this, 'login_message' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_heartbeat' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_heartbeat' ] ); } public function on_login( $cookie, $expire, $expiration, $user_id, $scheme, $token ) { if ( ! $token || ! $user_id ) { return; } $ip = Device::ip(); $ua = Device::user_agent(); list( $label, $type ) = Device::describe( $ua ); $now = Repository::now(); Repository::insert( [ 'user_id' => (int) $user_id, 'token_hash' => hash( 'sha256', $token ), 'device_id' => Device::id(), 'device_label' => $label, 'device_type' => $type, 'user_agent' => $ua, 'ip' => Settings::get( 'anonymize_ip' ) ? Device::anonymize( $ip ) : $ip, 'ip_net' => Device::network( $ip ), 'country' => Device::country(), 'created_at' => $now, 'last_seen' => $now, 'expires_at' => gmdate( 'Y-m-d H:i:s', (int) $expiration ), ] ); Enforcer::on_login( (int) $user_id ); } public function on_logout( $user_id ) { $token = $this->cookie_token(); if ( $token ) { Repository::end_by_token_hash( hash( 'sha256', $token ), 'logout' ); } } public function on_password_reset( $user ) { foreach ( Repository::open_for_user( $user->ID ) as $session ) { Repository::end( (int) $session->id, 'password' ); } } public function on_user_deleted( $user_id ) { global $wpdb; $wpdb->delete( \NetDesign\SessionGuard\Install\Schema::sessions_table(), [ 'user_id' => (int) $user_id ] ); $wpdb->delete( \NetDesign\SessionGuard\Install\Schema::flags_table(), [ 'user_id' => (int) $user_id ] ); \NetDesign\SessionGuard\Detection\OverlapRepository::delete_for_user( $user_id ); \NetDesign\SessionGuard\Detection\NoteRepository::delete_for_user( $user_id ); } /** * A logged-in cookie that no longer authenticates means the session was * destroyed. If we destroyed it, tell the visitor why (one query, only in * this rare case). */ public function detect_kicked_cookie() { if ( is_user_logged_in() || wp_doing_ajax() || empty( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) { return; } $token = $this->cookie_token(); if ( ! $token ) { return; } $row = Repository::find_by_token_hash( hash( 'sha256', $token ) ); if ( $row && in_array( $row->end_reason, [ 'policy', 'admin' ], true ) ) { $this->kicked_notice = true; wp_clear_auth_cookie(); } } public function print_kicked_notice() { if ( ! $this->kicked_notice ) { return; } printf( '