Login_Audit_Entity::get_table_name(),
'singular' => Login_Audit_Entity::get_table_name(),
'ajax' => false,
)
);
}
/**
* Register the submenu page.
*
* @return void
*
* @since 5.3.0
*/
public static function menu_add(): void {
$hook = \add_submenu_page(
Logs_List::MENU_SLUG,
\esc_html__( 'Login Audit', '0-day-analytics' ),
\esc_html__( 'Login Audit', '0-day-analytics' ),
'manage_options',
self::MENU_SLUG,
array( Login_Audit_View::class, 'render' ),
8
);
self::add_screen_options( $hook );
\add_filter( 'manage_' . $hook . '_columns', array( self::class, 'manage_columns' ) );
\add_action( 'load-' . $hook, array( Settings::class, 'aadvana_common_help' ) );
\add_action( 'load-' . $hook, array( self::class, 'process_actions_load' ) );
}
/**
* Handle bulk and row actions during the early page load hook.
*
* @return void
*
* @since 5.3.0
*/
public static function process_actions_load(): void {
if ( ! \current_user_can( 'manage_options' ) ) {
return;
}
$table = new self();
$table->handle_table_actions();
}
/**
* Returns the screen per page title.
*
* @return string
*
* @since 5.3.0
*/
private static function get_screen_per_page_title(): string {
return \__( 'Number of login events to show', '0-day-analytics' );
}
/**
* Adds columns to the screen options screen.
*
* @param array $columns Array of column names.
*
* @return array
*
* @since 5.3.0
*/
public static function manage_columns( $columns ): array {
if ( empty( self::$admin_columns ) ) {
$admin_columns = Login_Audit_Entity::get_column_names_admin();
$table_columns = array(
'cb' => '',
);
self::$admin_columns = \array_merge( $table_columns, $admin_columns, $columns );
}
return self::$admin_columns;
}
/**
* Prepares the list of items for displaying.
*
* @since 5.3.0
*/
public function prepare_items() {
$per_page = self::get_screen_option_per_page();
$current_page = $this->get_pagenum();
$offset = ( $current_page > 1 ) ? $per_page * ( $current_page - 1 ) : 0;
$search_string = self::escaped_search_input();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$event_type = isset( $_REQUEST['event_type'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['event_type'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$severity = isset( $_REQUEST['severity'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['severity'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby = ( isset( $_GET['orderby'] ) && '' !== trim( (string) $_GET['orderby'] ) ) ? \esc_sql( \sanitize_text_field( \wp_unslash( $_GET['orderby'] ) ) ) : 'created_at';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order = ( isset( $_GET['order'] ) && '' !== trim( (string) $_GET['order'] ) ) ? \esc_sql( \sanitize_text_field( \wp_unslash( $_GET['order'] ) ) ) : 'DESC';
$items = $this->fetch_table_data(
array(
'search_string' => $search_string,
'offset' => $offset,
'per_page' => $per_page,
'orderby' => $orderby,
'order' => $order,
'event_type' => $event_type,
'severity' => $severity,
)
);
$columns = self::manage_columns( array() );
$hidden = \get_user_option( 'manage' . WP_Helper::get_wp_screen()->id . 'columnshidden', false );
if ( ! $hidden ) {
$hidden = array();
}
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );
$this->items = $items;
$this->set_pagination_args(
array(
'total_items' => $this->count,
'per_page' => $per_page,
'total_pages' => ceil( $this->count / $per_page ),
)
);
}
/**
* Get a list of columns.
*
* @since 5.3.0
*
* @return array
*/
public function get_columns() {
return self::manage_columns( array() );
}
/**
* Get sortable columns.
*
* @since 5.3.0
*
* @return array
*/
public function get_sortable_columns() {
return array(
'created_at' => array( 'created_at', true ),
'event_type' => array( 'event_type', false ),
'severity' => array( 'severity', false ),
'username' => array( 'username', false ),
'ip_hash' => array( 'ip_hash', false ),
'description' => array( 'description', false ),
);
}
/**
* Text displayed when no items available.
*
* @since 5.3.0
*
* @return void
*/
public function no_items() {
\esc_html_e( 'No login or authentication events recorded.', '0-day-analytics' );
}
/**
* Fetch table data.
*
* @param array $args Arguments for query.
*
* @return array
*
* @since 5.3.0
*/
public function fetch_table_data( array $args = array() ) {
$parsed_args = \wp_parse_args(
$args,
array(
'offset' => 0,
'search_string' => self::escaped_search_input(),
'per_page' => self::get_screen_option_per_page(),
'orderby' => 'created_at',
'order' => 'DESC',
'event_type' => '',
'severity' => '',
)
);
$total = 0;
$result = Login_Audit_Entity::get_list_data(
array(
'search_string' => $parsed_args['search_string'],
'event_type' => ( '' !== $parsed_args['event_type'] ) ? $parsed_args['event_type'] : null,
'severity' => ( '' !== $parsed_args['severity'] ) ? $parsed_args['severity'] : null,
'orderby' => $parsed_args['orderby'],
'order' => $parsed_args['order'],
'per_page' => (int) $parsed_args['per_page'],
'offset' => (int) $parsed_args['offset'],
),
$total
);
$this->count = $total;
return $result;
}
/**
* Render a column when no column specific method exists.
*
* @param array $item Row data.
* @param string $column_name Column name.
*
* @return mixed
*
* @since 5.3.0
*/
public function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'created_at':
$query_args_view_data = array();
$query_args_view_data['_wpnonce'] = \wp_create_nonce( 'bulk-' . $this->_args['plural'] );
$delete_url = \add_query_arg(
array(
'action' => 'delete',
'advan_' . Login_Audit_Entity::get_table_name() => $item['id'],
self::SEARCH_INPUT => self::escaped_search_input(),
'_wpnonce' => $query_args_view_data['_wpnonce'],
)
);
$actions['view'] = '' . \esc_html__( 'View', '0-day-analytics' ) . '';
$actions['delete'] = '' . \esc_html__( 'Delete', '0-day-analytics' ) . '';
$time_format = 'g:i a';
$created_at_ts = strtotime( $item['created_at'] );
if ( false === $created_at_ts ) {
return \esc_html( $item['created_at'] );
}
$event_datetime_utc = \gmdate( 'Y-m-d H:i:s', $created_at_ts );
$timezone_local = \wp_timezone();
$event_local = \get_date_from_gmt( $event_datetime_utc, 'Y-m-d' );
$today_local = ( new \DateTimeImmutable( 'now', $timezone_local ) )->format( 'Y-m-d' );
$yesterday_local = ( new \DateTimeImmutable( 'yesterday', $timezone_local ) )->format( 'Y-m-d' );
if ( \get_date_from_gmt( $event_datetime_utc, 'P' ) !== \get_date_from_gmt( 'now', 'P' ) ) {
$time_format .= ' (P)';
}
$event_time_local = \get_date_from_gmt( $event_datetime_utc, $time_format );
if ( $event_local === $today_local ) {
$date = sprintf(
\__( 'Today at %s', '0-day-analytics' ),
$event_time_local
);
} elseif ( $event_local === $yesterday_local ) {
$date = sprintf(
\__( 'Yesterday at %s', '0-day-analytics' ),
$event_time_local
);
} else {
$date = sprintf(
\__( '%1$s at %2$s', '0-day-analytics' ),
\get_date_from_gmt( $event_datetime_utc, 'F jS' ),
$event_time_local
);
}
$time = sprintf(
'',
\esc_attr( gmdate( 'c', $created_at_ts ) ),
\esc_html( $date )
);
$until = $created_at_ts - time();
$ago = sprintf(
\__( '%s ago', '0-day-analytics' ),
WP_Helper::interval( abs( $until ) )
);
return sprintf(
' %s
%s',
\esc_html( $ago ),
$time
) . $this->row_actions( $actions );
case 'event_type':
return self::event_type_badge( $item['event_type'] );
case 'severity':
return self::severity_badge( $item['severity'] );
case 'username':
$user_id = (int) ( $item['user_id'] ?? 0 );
$username = \esc_html( $item['username'] ?? '' );
if ( $user_id > 0 ) {
$user = \get_userdata( $user_id );
if ( $user instanceof \WP_User ) {
$edit_url = \get_edit_user_link( $user_id );
$display = \esc_html( $user->display_name ?: $user->user_login );
return '' . $display . '';
}
}
return $username ?: '—';
case 'ip_hash':
if ( ! empty( $item['ip_hash'] ) ) {
return '' . \esc_html( substr( $item['ip_hash'], 0, 16 ) . '…' ) . '';
}
return '—';
case 'user_agent':
if ( empty( $item['user_agent'] ) ) {
return '—';
}
$ua = \esc_html( $item['user_agent'] );
if ( strlen( $ua ) > 80 ) {
$short = substr( $ua, 0, 80 ) . '…';
return '' . \esc_html( $short ) . '';
}
return '' . $ua . '';
case 'description':
$desc = \esc_html( $item['description'] );
// Link "user #ID" references.
$desc = \preg_replace_callback(
'/user\s#(\d+)/',
static function ( $matches ) {
$uid = (int) $matches[1];
if ( $uid < 1 ) {
return $matches[0];
}
$user = \get_userdata( $uid );
if ( ! $user instanceof \WP_User ) {
return $matches[0];
}
$edit_url = \get_edit_user_link( $uid );
$display = \esc_html( $user->display_name ?: $user->user_login );
return '' . $display . '';
},
$desc
);
return '' . $desc . '';
default:
return isset( $item[ $column_name ] ) ? \esc_html( (string) $item[ $column_name ] ) : '';
}
}
/**
* Get value for checkbox column.
*
* @param object|array $item Row data.
*
* @return string
*
* @since 5.3.0
*/
protected function column_cb( $item ) {
return sprintf(
''
. '',
(int) $item['id'],
sprintf( \__( 'Select %s', '0-day-analytics' ), 'id' ),
\esc_attr( Login_Audit_Entity::get_table_name() )
);
}
/**
* Returns bulk actions.
*
* @return array
*
* @since 5.3.0
*/
public function get_bulk_actions() {
return array(
'delete' => \__( 'Delete Records', '0-day-analytics' ),
);
}
/**
* Process actions triggered by the user.
*
* @since 5.3.0
*/
protected function handle_table_actions() {
if ( ! \is_user_logged_in() || ! \current_user_can( 'manage_options' ) ) {
return;
}
$field_name = 'advan_' . Login_Audit_Entity::get_table_name();
// Single row delete.
if ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) {
if ( isset( $_REQUEST[ $field_name ] ) && ! \is_array( $_REQUEST[ $field_name ] ) ) {
\check_admin_referer( 'bulk-' . $this->_args['plural'] );
$id = \absint( $_REQUEST[ $field_name ] );
if ( $id > 0 ) {
Login_Audit_Entity::delete_by_id( $id );
}
$redirect = \remove_query_arg(
array( 'action', $field_name, '_wpnonce' )
);
\wp_safe_redirect( $redirect );
exit;
}
}
// Bulk delete.
$action = $this->current_action();
$entries = isset( $_REQUEST[ $field_name ] ) && \is_array( $_REQUEST[ $field_name ] )
? \array_map( 'absint', $_REQUEST[ $field_name ] )
: array();
if ( 'delete' === $action && ! empty( $entries ) ) {
\check_admin_referer( 'bulk-' . $this->_args['plural'] );
Login_Audit_Entity::delete_by_ids( $entries );
$redirect = \remove_query_arg(
array( 'action', 'action2', $field_name, '_wpnonce' )
);
\wp_safe_redirect( $redirect );
exit;
}
}
/**
* Generates content for a single row of the table.
*
* @param object|array $item The current item.
*
* @since 5.3.0
*/
public function single_row( $item ) {
$classes = 'la-severity-' . \esc_attr( $item['severity'] ?? 'low' );
echo '