WP_Fatals_Entity::get_table_name(), // Plural value used for labels and the objects being listed. 'singular' => WP_Fatals_Entity::get_table_name(), // Singular label for an object being listed, e.g. 'post'. 'ajax' => false, // If true, the parent class will call the _js_vars() method in the footer. ) ); } /** * Inits class hooks. That is called every time - not in some specific environment set. * * @return void * * @since 3.8.0 */ public static function init() { \add_action( 'admin_post_' . self::PLUGIN_FILTER_ACTION, array( Fatals_View::class, 'plugin_filter_action' ) ); } /** * Truncates the fatals table from CRON job * * @return void * * @since 3.8.0 */ public static function truncate_fatals_table() { Common_Table::truncate_table( null, WP_Fatals_Entity::get_table_name() ); } /** * Adds the module to the main plugin menu * * @return void * * @since 2.8.1 */ public static function menu_add() { $fatals_hook = \add_submenu_page( Logs_List::MENU_SLUG, ADVAN_INNER_NAME, \esc_html__( 'PHP error viewer', '0-day-analytics' ), 'manage_options', self::FATALS_MENU_SLUG, array( Fatals_View::class, 'analytics_fatals_page' ), 3 ); self::add_screen_options( $fatals_hook ); \add_filter( 'manage_' . $fatals_hook . '_columns', array( self::class, 'manage_columns' ) ); \add_action( 'load-' . $fatals_hook, array( Settings::class, 'aadvana_common_help' ) ); // Process actions early to allow safe redirects before any output. \add_action( 'load-' . $fatals_hook, array( self::class, 'process_actions_load' ) ); } /** * Handle bulk and row actions during the early page load hook. * * @return void * * @since 4.2.0 */ public static function process_actions_load() { if ( ! \current_user_can( 'manage_options' ) ) { return; } $table = new self( '' ); $table->handle_table_actions(); } /** * Prepares the list of items for displaying. * * Query, filter data, handle sorting, and pagination, and any other data-manipulation required prior to rendering * * @since 1.0.0 */ public function prepare_items() { // Actions are processed during load- to avoid header warnings. $per_page = self::get_screen_option_per_page(); $current_page = $this->get_pagenum(); if ( 1 < $current_page ) { $offset = $per_page * ( $current_page - 1 ); } else { $offset = 0; } $search_string = self::escaped_search_input(); /* phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading view-only filter state; sanitized below. */ if ( isset( $_REQUEST['plugin'] ) && ! empty( $_REQUEST['plugin'] ) ) { if ( -1 === (int) $_REQUEST['plugin'] ) { $plugin = -1; } else { $plugin_raw = \sanitize_text_field( \wp_unslash( $_REQUEST['plugin'] ) ); // Validate plugin slug format (alphanumeric, dashes, underscores, max 255 chars). if ( preg_match( '/^[a-zA-Z0-9\-_]{1,255}$/', $plugin_raw ) ) { $plugin = $plugin_raw; } else { $plugin = ''; } } } else { $plugin = ''; } // $wpdb_table = $this->get_table_name(); /* phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading view-only sorting params */ $orderby = ( isset( $_GET['orderby'] ) && '' !== trim( (string) $_GET['orderby'] ) ) ? \esc_sql( \sanitize_text_field( \wp_unslash( $_GET['orderby'] ) ) ) : 'datetime'; /* phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading view-only sorting params */ $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, // 'wpdb_table' => $wpdb_table, 'orderby' => $orderby, 'order' => $order, 'plugin' => $plugin, ) ); $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; // Set the pagination. $this->set_pagination_args( array( 'total_items' => $this->count, 'per_page' => self::get_screen_option_per_page(), 'total_pages' => ceil( $this->count / self::get_screen_option_per_page() ), ) ); } /** * Get a list of columns. The format is: * 'internal-name' => 'Title' * * @since 1.0.0 * * @return array */ public function get_columns() { return self::manage_columns( array() ); } /** * Get a list of sortable columns. The format is: * 'internal-name' => 'orderby' * or * 'internal-name' => array( 'orderby', true ) * * The second format will make the initial sorting order be descending * * @since 1.1.0 * * @return array */ public function get_sortable_columns() { $first6_columns = array_keys( WP_Fatals_Entity::get_column_names_admin() ); $sortable_columns = array(); /** * Actual sorting still needs to be done by prepare_items. * specify which columns should have the sort icon. * * The second bool param sets the column sort order - true ASC, false - DESC or unsorted. */ foreach ( $first6_columns as $value ) { $sortable_columns[ $value ] = array( $value, false ); } return $sortable_columns; } /** * Text displayed when no user data is available * * @since 3.8.0 * * @return void */ public function no_items() { \esc_html_e( 'No rows', '0-day-analytics' ); } /** * Fetch table data from the WordPress database. * * @param array $args - The arguments collected / passed. * * @since 3.8.0 * * @return Array */ public function fetch_table_data( array $args = array() ) { global $wpdb; // Parse. $parsed_args = \wp_parse_args( $args, array( 'offset' => 0, 'search_string' => self::escaped_search_input(), 'per_page' => self::get_screen_option_per_page(), // 'wpdb_table' => $this->get_table_name(), 'search_sql' => '', 'orderby' => 'datetime', 'order' => 'DESC', 'count' => false, 'plugin' => '', ) ); $search_string = $wpdb->esc_like( \sanitize_text_field( \wp_unslash( $parsed_args['search_string'] ) ) ); $offset = (int) $parsed_args['offset']; $per_page = (int) $parsed_args['per_page']; // $wpdb_table = \sanitize_key( (string) $parsed_args['wpdb_table'] ); $orderby = \esc_sql( \sanitize_text_field( \wp_unslash( $parsed_args['orderby'] ) ) ); $order = \sanitize_text_field( \wp_unslash( $parsed_args['order'] ) ); $plugin = \sanitize_text_field( \wp_unslash( $parsed_args['plugin'] ) ); $order = self::get_order( $order ); $orderby = self::get_order_by( $orderby ); if ( '0' === (string) $plugin ) { $plugin = ''; } $search_sql = ''; if ( '' !== $search_string ) { $like = '%' . $wpdb->esc_like( $search_string ) . '%'; $search_parts = array(); $search_parts[] = $wpdb->prepare( 'id LIKE %s', $like ); foreach ( array_keys( WP_Fatals_Entity::get_all_columns() ) as $value ) { // Column names come from a trusted source. Only values are prepared. $search_parts[] = $value . ' ' . $wpdb->prepare( 'LIKE %s', $like ); } $search_sql = ' AND (' . implode( ' OR ', $search_parts ) . ') '; } if ( '' !== $plugin && -1 !== (int) $plugin ) { $search_sql .= $wpdb->prepare( ' AND source_slug = %s ', (string) $plugin ); } $wpdb_table = $this->get_table_name(); $query = 'SELECT ' . implode( ', ', \array_keys( WP_Fatals_Entity::get_fields() ) ) . ' FROM ' . $wpdb_table . ' WHERE 1=1 ' . $search_sql . ' ORDER BY ' . $orderby . ' ' . $order; $query .= $wpdb->prepare( ' LIMIT %d OFFSET %d;', $per_page, $offset ); // query output_type will be an associative array with ARRAY_A. $query_results = WP_Fatals_Entity::get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Counting rows for pagination; table name is trusted; dynamic WHERE clause values are prepared above. $this->count = (int) WP_Fatals_Entity::get_var( 'SELECT COUNT(id) FROM ' . $wpdb_table . ' WHERE 1=1 ' . $search_sql ); // return result array to prepare_items. return $query_results; } /** * Filter the table data based on the user search key * * @since 1.0.0 * * @param array $table_data - The data from the row. * @param string $search_key - The search key. * * @return array */ public function filter_table_data( $table_data, $search_key ) { $filtered_table_data = array_values( array_filter( $table_data, function( $row ) use ( $search_key ) { foreach ( $row as $row_val ) { if ( stripos( $row_val, $search_key ) !== false ) { return true; } } } ) ); return $filtered_table_data; } /** * Render a column when no column specific method exists. * * Use that method for common rendering and separate columns logic in different methods. See below. * * @param array $item - Array with the current row values. * @param string $column_name - The name of the currently processed column. * * @return mixed * * @since 2.7.0 */ public function column_default( $item, $column_name ) { switch ( $column_name ) { case 'user_id': if ( isset( $item[ $column_name ] ) && ! empty( $item[ $column_name ] ) && 0 !== $item[ $column_name ] ) { $user = \get_user_by( 'id', $item[ $column_name ] ); if ( $user ) { return '' . \esc_html( $user->display_name ) . ' (' . \esc_html( $user->user_email ) . ')'; } else { return \esc_html__( 'Unknown or deleted user', '0-day-analytics' ); } } else { return \esc_html__( 'WP System or Anonymous user', '0-day-analytics' ); } case 'message': $message = '
'; $message .= '' . \esc_html( $item[ $column_name ] ) . ''; if ( isset( $item['sub_items'] ) && ! empty( $item['sub_items'] ) ) { $message .= '
'; $reversed_details = \array_reverse( $item['sub_items'] ); $message .= ''; } return $message; case 'error_file': if ( isset( $item['error_file'] ) && ! empty( $item['error_file'] ) ) { $query_array = array( '_wpnonce' => \wp_create_nonce( 'source-view' ), 'action' => 'log_source_view', ); $source_link = ''; $query_array['error_file'] = $item['error_file']; $query_array['error_line'] = 1; if ( isset( $item['error_line'] ) && ! empty( $item['error_line'] ) ) { $query_array['error_line'] = $item['error_line']; } $query_array['TB_iframe'] = 'true'; $view_url = \esc_url_raw( \add_query_arg( $query_array, \admin_url( 'admin-ajax.php' ) ) ); $title = __( 'Viewing: ', '0-day-analytics' ) . $query_array['error_file']; $source_link = ' ' . \esc_html( $query_array['error_file'] . ':' . (string) $query_array['error_line'] ) . '
'; return $source_link; } return isset( $item['error_file'] ) ? \esc_html( (string) $item['error_file'] ) : ''; case 'ip': if ( \is_string( $item['ip'] ) ) { $ips = \explode( ',', $item['ip'] ); $ips = array_map( 'esc_html', array_map( 'trim', $ips ) ); return implode( '
', $ips ); } return \esc_html( (string) $item['ip'] ); case 'severity': case 'type_env': case 'user_roles': case 'version_text': case 'source_type': case 'repeating': return '' . \esc_html( $item[ $column_name ] ) . ''; case 'datetime': $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_' . self::$table::get_name() => $item['id'], self::SEARCH_INPUT => self::escaped_search_input(), '_wpnonce' => $query_args_view_data['_wpnonce'], ) ); $actions['delete'] = '' . \esc_html__( 'Delete', '0-day-analytics' ) . ''; $actions['details'] = '' . \esc_html__( 'View', '0-day-analytics' ) . ''; $time_format = 'g:i a'; $item['date_added'] = (int) $item['datetime']; $event_datetime_utc = \gmdate( 'Y-m-d H:i:s', $item['date_added'] ); $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' ); $tomorrow_local = ( new \DateTimeImmutable( 'tomorrow', $timezone_local ) )->format( 'Y-m-d' ); $yesterday_local = ( new \DateTimeImmutable( 'yesterday', $timezone_local ) )->format( 'Y-m-d' ); // If the offset of the date of the event is different from the offset of the site, add a marker. 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( /* translators: %s: Time */ __( 'Today at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $tomorrow_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Tomorrow at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $yesterday_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Yesterday at %s', '0-day-analytics' ), $event_time_local, ); } else { $date = sprintf( /* translators: 1: Date, 2: Time */ __( '%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', $item['date_added'] ) ), \esc_html( $date ) ); $until = $item['date_added'] - time(); if ( $until < 0 ) { $ago = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( '%s ago', '0-day-analytics' ), WP_Helper::interval( abs( $until ) ) ); return sprintf( ' %s
%s', esc_html( $ago ), $time, ) . $this->row_actions( $actions ); } elseif ( 0 === $until ) { $in = __( 'Now', '0-day-analytics' ); } else { $in = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( 'In %s', '0-day-analytics' ), WP_Helper::interval( $until ), ); } return sprintf( ' %s
%s', \esc_html( $in ), $time, ) . $this->row_actions( $actions ); case 'source': if ( ! empty( $item['source'] ) ) { return '' . \esc_html( $item['source'] ) . ''; } else { return \esc_html__( 'Unknown', '0-day-analytics' ); } } // Default return for unhandled columns. return isset( $item[ $column_name ] ) ? \esc_html( $item[ $column_name ] ) : ''; } /** * Get value for checkbox column. * * The special 'cb' column * * @param object $item - A row's data. * * @return string Text to be placed inside the column < td > . * * @since 3.8.0 */ protected function column_cb( $item ) { return sprintf( '' . '' ); } /** * Returns an associative array containing the bulk actions * * @since 1.0.0 * * @return array */ public function get_bulk_actions() { /** * On hitting apply in bulk actions the url params are set as * ?action=bulk-download&paged=1&action2=-1 * * Action and action2 are set based on the triggers above or below the table */ $actions = array( 'delete' => __( 'Delete Records', '0-day-analytics' ), ); return $actions; } /** * Process actions triggered by the user * * @since 1.0.0 */ public function handle_table_actions() { /** * Note: Table bulk_actions can be identified by checking $_REQUEST['action'] and $_REQUEST['action2'] * * Action - is set if checkbox from top-most select-all is set, otherwise returns -1 * Action2 - is set if checkbox the bottom-most select-all checkbox is set, otherwise returns -1 */ if ( \is_user_logged_in() && \current_user_can( 'manage_options' ) ) { // check for individual row actions. $the_table_action = $this->current_action(); // check for table bulk actions. if ( ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) ) { // verify the nonce. /** * Note: the nonce field is set by the parent class * wp_nonce_field( 'bulk-' . $this->_args['plural'] ); */ WP_Helper::verify_admin_nonce( 'bulk-' . $this->_args['plural'] ); if ( isset( $_REQUEST[ 'advan_' . self::$table::get_name() ] ) ) { foreach ( (array) $_REQUEST[ 'advan_' . self::$table::get_name() ] as $id ) { self::$table::delete_by_id( (int) $id ); } } $redirect = \remove_query_arg( array( 'delete', '_wpnonce', 'advan_' . self::$table::get_name(), 'action' ), \add_query_arg( array( self::SEARCH_INPUT => self::escaped_search_input(), 'paged' => $this->get_pagenum(), 'page' => self::FATALS_MENU_SLUG, 'show_table' => self::$table::get_name(), ), \admin_url( 'admin.php' ) ) ); wp_safe_redirect( $redirect ); exit; } } } /** * View a license information. * * @since 1.0.0 * * @param int $table_id - Record ID. */ public function page_view_data( $table_id ) { // Render/edit view is handled by the respective view class. } /** * Table navigation. * * @param string $which - Position of the nav. * * @since 1.1.0 */ public function extra_tablenav( $which ) { /* phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading view-only filter state; sanitized below. */ if ( isset( $_REQUEST['plugin'] ) && ! empty( $_REQUEST['plugin'] ) ) { if ( -1 === (int) $_REQUEST['plugin'] ) { $plugin = -1; } else { $plugin = \sanitize_text_field( \wp_unslash( $_REQUEST['plugin'] ) ); } } else { $plugin = 0; } ?>
count > 0 ) { ?>
| | | | |
'', // to display the checkbox. ); self::$admin_columns = \array_merge( $table_columns, $screen_options, $columns ); } return self::$admin_columns; } /** * Generates content for a single row of the table. * * @param object|array $item - The current item. * * @since 3.3.1 */ public function single_row( $item ) { $classes = ''; if ( isset( $item['fatal_status'] ) && ! empty( $item['fatal_status'] ) ) { $classes .= ' ' . $item['fatal_status']; } echo ''; $this->single_row_columns( $item ); echo ''; } /** * Generates the table navigation above or below the table * * @param string $which - Holds info about the top and bottom navigation. * * @since 3.3.1 */ 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 ); ?>