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 ''; $this->single_row_columns( $item ); echo ''; } /** * Generates the table navigation above or below the table. * * @param string $which Top or bottom. * * @since 5.3.0 */ public function display_tablenav( $which ) { if ( 'top' === $which ) { \wp_nonce_field( 'bulk-' . $this->_args['plural'] ); ?>
has_items() ) { ?>
bulk_actions( $which ); ?>
extra_tablenav( $which ); $this->pagination( $which ); ?>
\__( 'All Event Types', '0-day-analytics' ) ) + $labels; $severities = array( '' => \__( 'All Severities', '0-day-analytics' ), 'low' => \__( 'Low', '0-day-analytics' ), 'medium' => \__( 'Medium', '0-day-analytics' ), 'high' => \__( 'High', '0-day-analytics' ), 'critical' => \__( 'Critical', '0-day-analytics' ), ); ?>
'post-query-submit' ) ); ?>
'#00a32a', Login_Audit_Entity::EVENT_LOGIN_FAILED => '#d63638', Login_Audit_Entity::EVENT_LOGOUT => '#2271b1', Login_Audit_Entity::EVENT_PASSWORD_RESET => '#e65100', Login_Audit_Entity::EVENT_PASSWORD_REQUEST => '#dba617', Login_Audit_Entity::EVENT_COOKIE_VALID => '#00a32a', Login_Audit_Entity::EVENT_COOKIE_INVALID => '#d63638', Login_Audit_Entity::EVENT_APP_PASSWORD => '#8c5e58', Login_Audit_Entity::EVENT_SESSION_DESTROY => '#e65100', Login_Audit_Entity::EVENT_USER_LOCKED => '#d63638', Login_Audit_Entity::EVENT_LOGIN_COOKIE => '#9bad71', ); $labels = Login_Audit_Entity::get_event_type_labels(); $color = $colors[ $type ] ?? '#646970'; $label = $labels[ $type ] ?? $type; return sprintf( '%s', \esc_attr( $color ), \esc_html( $label ) ); } /** * Render a colored severity badge. * * @param string $severity Severity level. * * @return string * * @since 5.3.0 */ private static function severity_badge( string $severity ): string { $colors = array( 'low' => '#a7aaad', 'medium' => '#dba617', 'high' => '#e65100', 'critical' => '#d63638', ); $color = $colors[ $severity ] ?? '#646970'; return sprintf( '%s', \esc_attr( $color ), \esc_html( $severity ) ); } } }