' . esc_html__( 'Your sessions', '404ops-activity-log' ) . '';
self::render_own_sessions();
echo '
' . esc_html__( 'Signed-in users', '404ops-activity-log' ) . '
';
self::render_user_table();
echo '' . esc_html__( 'Session policy', '404ops-activity-log' ) . '
';
self::render_policy_form();
}
/**
* The notice for the result an action redirected back with. Only a
* known code is ever shown; anything else in the URL shows nothing.
*/
private static function render_result_notice(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only.
$code = isset( $_GET[ SessionActions::RESULT_ARG ] ) ? sanitize_key( wp_unslash( $_GET[ SessionActions::RESULT_ARG ] ) ) : '';
if ( ! in_array( $code, SessionActions::RESULTS, true ) ) {
return;
}
$messages = array(
'destroyed_one' => __( 'The session was logged out.', '404ops-activity-log' ),
'destroyed_user' => __( 'All of that user\'s sessions were logged out.', '404ops-activity-log' ),
'destroyed_others' => __( 'You are now logged out everywhere else. This session was kept.', '404ops-activity-log' ),
'policy_saved' => __( 'Session policy saved.', '404ops-activity-log' ),
'bad_request' => __( 'That request was not understood.', '404ops-activity-log' ),
'bad_nonce' => __( 'The link has expired. Reload the page and try again.', '404ops-activity-log' ),
'forbidden' => __( 'You are not allowed to manage that user\'s sessions.', '404ops-activity-log' ),
'own_current' => __( 'That is the session you are using now. To end it, use Log Out.', '404ops-activity-log' ),
'gone' => __( 'That session no longer exists. It may have expired or been logged out already.', '404ops-activity-log' ),
'unsupported' => __( 'This site stores sessions in a custom session manager, so single sessions cannot be logged out from here.', '404ops-activity-log' ),
'no_current_session' => __( 'This request is not signed in with a browser session, so there is nothing to keep.', '404ops-activity-log' ),
);
$is_error = ! in_array( $code, array( 'destroyed_one', 'destroyed_user', 'destroyed_others', 'policy_saved' ), true );
echo '' . esc_html( $messages[ $code ] ) . '
';
}
/**
* The acting user's own sessions and "log out everywhere else".
*/
private static function render_own_sessions(): void {
$me = wp_get_current_user();
$others = max( 0, count( \WP_Session_Tokens::get_instance( $me->ID )->get_all() ) - 1 );
echo '' . esc_html(
sprintf(
/* translators: %d: number of other sessions. */
_n( 'You are signed in in %d other place.', 'You are signed in in %d other places.', $others, '404ops-activity-log' ),
$others
)
) . '
';
if ( $others > 0 ) {
self::action_form(
SessionActions::OP_DESTROY_OTHERS,
(int) $me->ID,
'',
__( 'Log out everywhere else', '404ops-activity-log' ),
__( 'Ends every other session of yours. This session stays signed in.', '404ops-activity-log' )
);
}
}
/**
* Every user with a stored session on this page of the list.
*/
private static function render_user_table(): void {
if ( ! SessionStore::supported() ) {
echo '' . esc_html(
sprintf(
/* translators: %s: PHP class name of the session manager. */
__( 'Sessions on this site are stored by a custom session manager (%s), which this screen cannot list. "Log out everywhere else" and the idle timeout still work; single-session logout and the concurrent-session limit do not.', '404ops-activity-log' ),
SessionStore::manager_class()
)
) . '
';
return;
}
if ( is_multisite() && ! current_user_can( 'manage_network_users' ) ) {
echo '' . esc_html__( 'On a network, sessions are shared by every site, so only a network administrator can manage other users\' sessions.', '404ops-activity-log' ) . '
';
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- pagination only.
$page = isset( $_GET['sessions_page'] ) ? max( 1, absint( $_GET['sessions_page'] ) ) : 1;
$result = SessionStore::users_with_sessions( $page, self::PER_PAGE );
$now = time();
$mine = SessionStore::current_verifier();
$rows = 0;
echo '';
echo '';
foreach ( array(
__( 'User', '404ops-activity-log' ),
__( 'Signed in', '404ops-activity-log' ),
__( 'Last active', '404ops-activity-log' ),
__( 'Expires', '404ops-activity-log' ),
__( 'IP Address', '404ops-activity-log' ),
__( 'Browser', '404ops-activity-log' ),
__( 'Actions', '404ops-activity-log' ),
) as $heading ) {
echo '| ' . esc_html( $heading ) . ' | ';
}
echo '
';
foreach ( $result['users'] as $user ) {
$sessions = SessionStore::sessions( (int) $user->ID );
if ( array() === $sessions ) {
continue;
}
$is_me = get_current_user_id() === (int) $user->ID;
$can_manage = $is_me || SessionActions::may_manage( $user );
$idle = SessionPolicy::idle_seconds_for( $user );
$first = true;
foreach ( $sessions as $verifier => $session ) {
++$rows;
$is_current = $is_me && '' !== $mine && hash_equals( $mine, (string) $verifier );
echo '';
echo '| ';
if ( $first ) {
echo wp_kses_post( self::user_cell( $user ) );
// The current user's other-session action is offered above the table.
if ( ! $is_me && $can_manage ) {
self::action_form( SessionActions::OP_DESTROY_USER, (int) $user->ID, '', __( 'Log out all sessions', '404ops-activity-log' ), '' );
}
}
echo ' | ';
echo '' . esc_html( self::when( $session['login'] ?? null ) ) . ' | ';
echo '' . esc_html( self::last_active( $session, $idle, $now ) ) . ' | ';
echo '' . esc_html( self::when( $session['expiration'] ?? null ) ) . ' | ';
echo '' . esc_html( SessionLog::ip_as_logged( $session['ip'] ?? null ) ?? '—' ) . ' | ';
echo '' . esc_html( self::browser( $session['ua'] ?? null ) ) . ' | ';
echo '';
if ( $is_current ) {
echo '' . esc_html__( 'This session', '404ops-activity-log' ) . '';
} elseif ( $can_manage ) {
self::action_form(
SessionActions::OP_DESTROY_ONE,
(int) $user->ID,
SessionStore::handle( (int) $user->ID, (string) $verifier ),
__( 'Log out', '404ops-activity-log' ),
''
);
}
echo ' | ';
echo '
';
$first = false;
}
}
echo '
';
if ( 0 === $rows ) {
echo '' . esc_html__( 'Nobody else is signed in.', '404ops-activity-log' ) . '
';
}
$pages = (int) ceil( $result['total'] / self::PER_PAGE );
if ( $pages > 1 ) {
echo '';
for ( $i = 1; $i <= $pages; $i++ ) {
$url = add_query_arg(
array(
'page' => '404ops-activity-log',
'tab' => 'sessions',
'sessions_page' => $i,
),
admin_url( 'admin.php' )
);
echo '' . esc_html( (string) $i ) . ' ';
}
echo '
';
}
}
/**
* The first cell of a user's group of rows.
*
* @param \WP_User $user The user.
*/
private static function user_cell( \WP_User $user ): string {
$html = '' . esc_html( $user->user_login ) . '';
if ( '' !== (string) $user->display_name && $user->display_name !== $user->user_login ) {
$html .= '
' . esc_html( (string) $user->display_name );
}
$html .= '
' . esc_html( implode( ', ', array_map( 'translate_user_role', self::role_names( $user ) ) ) ) . '';
return $html;
}
/**
* @param \WP_User $user The user.
* @return string[] Display names of their roles.
*/
private static function role_names( \WP_User $user ): array {
$names = wp_roles()->get_names();
$out = array();
foreach ( (array) $user->roles as $role ) {
$out[] = $names[ $role ] ?? $role;
}
return $out;
}
/**
* One small POST form for one action.
*
* @param string $op SessionActions::OP_*.
* @param int $user_id The target user.
* @param string $handle The session handle, for OP_DESTROY_ONE.
* @param string $label Button text.
* @param string $description Help text; '' for none.
*/
private static function action_form( string $op, int $user_id, string $handle, string $label, string $description ): void {
echo '';
}
/**
* The policy form.
*/
private static function render_policy_form(): void {
$policy = SessionPolicy::get();
$editable = SessionActions::may_edit_policy();
if ( ! $editable ) {
echo '' . esc_html__( 'On a network, the session policy can be changed only by a network administrator, because sessions are shared by every site.', '404ops-activity-log' ) . '
';
}
echo '';
}
/**
* @param mixed $timestamp Unix time or nothing.
*/
private static function when( $timestamp ): string {
if ( ! is_numeric( $timestamp ) || (int) $timestamp <= 0 ) {
return '—';
}
return (string) wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $timestamp );
}
/**
* Last activity, and whether the idle timeout has already doomed it.
*
* @param array $session The session.
* @param int $idle The owner's idle timeout in seconds, 0 = none.
* @param int $now Current time.
*/
private static function last_active( array $session, int $idle, int $now ): string {
$last = $session[ SessionStore::LAST_ACTIVITY_KEY ] ?? null;
if ( ! is_numeric( $last ) ) {
return 0 === $idle ? __( 'Not tracked', '404ops-activity-log' ) : '—';
}
$text = self::when( $last );
if ( $idle > 0 && $now - (int) $last >= $idle + SessionPolicy::throttle_seconds( $idle ) ) {
$text .= ' — ' . __( 'idle, ends on next use', '404ops-activity-log' );
}
return $text;
}
/**
* @param mixed $ua Core's stored user agent.
*/
private static function browser( $ua ): string {
if ( ! is_string( $ua ) || '' === $ua ) {
return '—';
}
$ua = function_exists( 'mb_substr' ) ? mb_substr( $ua, 0, self::UA_LENGTH ) : substr( $ua, 0, self::UA_LENGTH );
return sanitize_text_field( $ua );
}
}