advausro_logs_table = $wpdb->prefix . 'advausro_role_management_logs'; $this->advausro_current_page = isset($_GET['paged']) && check_admin_referer('advausro_logs_nonce', 'advausro_logs_nonce_field') ? max(1, absint(wp_unslash($_GET['paged']))) : 1; $this->advausro_fetch_logs(); } /** * Fetches logs from the database with proper security measures * * @return void */ private function advausro_fetch_logs() { global $wpdb; // 🔐 Permission check - ensure user has manage_options capability if (!current_user_can('manage_options')) { return; } // 🔐 Verify nonce before processing any filters $nonce_verified = false; if (is_admin() && isset($_GET['advausro_filter_nonce'])) { $nonce_verified = wp_verify_nonce( sanitize_text_field(wp_unslash($_GET['advausro_filter_nonce'])), 'advausro_filter_action' ); } // 🛡️ Initialize variables with sanitized values $advausro_offset = absint(($this->advausro_current_page - 1) * $this->advausro_items_per_page); $advausro_where_clauses = []; $advausro_args = []; // 🛡️ Use prepare() with proper table name - FIXED: Use prepared statement // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is built from the local WordPress prefix. $advausro_query = "SELECT id, action, user_id, log_message, created_at FROM {$this->advausro_logs_table}"; // 🔍 Process filters only if nonce is verified if ($nonce_verified) { // 🔍 Search by keyword in 'action' or 'log_message' if (isset($_GET['s']) && !empty($_GET['s'])) { $search_term = sanitize_text_field(wp_unslash($_GET['s'])); if (!empty($search_term)) { $advausro_like_term = '%' . $wpdb->esc_like($search_term) . '%'; $advausro_where_clauses[] = "(action LIKE %s OR log_message LIKE %s)"; $advausro_args[] = $advausro_like_term; $advausro_args[] = $advausro_like_term; } } // 📌 Filter by event type with strict validation if (isset($_GET['event_type']) && !empty($_GET['event_type'])) { $allowed_actions = ['create', 'update', 'delete']; // Example allowed actions $event_type = sanitize_key(wp_unslash($_GET['event_type'])); if (in_array($event_type, $allowed_actions, true)) { $advausro_where_clauses[] = "action = %s"; $advausro_args[] = $event_type; } } // 📅 Filter by date range with strict validation if (!empty($_GET['from_date']) && !empty($_GET['to_date'])) { $from_date = sanitize_text_field(wp_unslash($_GET['from_date'])); $to_date = sanitize_text_field(wp_unslash($_GET['to_date'])); // Validate date format (YYYY-MM-DD) if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $from_date) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to_date) ) { $advausro_where_clauses[] = "created_at BETWEEN %s AND %s"; $advausro_args[] = $from_date . ' 00:00:00'; $advausro_args[] = $to_date . ' 23:59:59'; } } } // 🧱 Build WHERE clause if filters exist if (!empty($advausro_where_clauses)) { $advausro_query .= " WHERE " . implode(' AND ', $advausro_where_clauses); } // 🧭 Add ORDER BY and LIMIT with proper parameterization $advausro_query .= " ORDER BY created_at DESC LIMIT %d, %d"; $advausro_all_args = array_merge($advausro_args, [$advausro_offset, $this->advausro_items_per_page]); // 📊 Prepare count query for pagination - FIXED: Use prepared statement $advausro_count_query = "SELECT COUNT(*) FROM {$this->advausro_logs_table}"; if (!empty($advausro_where_clauses)) { $advausro_count_query .= " WHERE " . implode(' AND ', $advausro_where_clauses); } // ✅ Execute queries with proper error handling try { $this->advausro_logs = $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query string is prepared immediately here from sanitized parts. $wpdb->prepare($advausro_query, $advausro_all_args) ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Count query is assembled from validated fragments and prepared when placeholders exist. $this->advausro_total_items = $wpdb->get_var( !empty($advausro_args) ? $wpdb->prepare($advausro_count_query, $advausro_args) : $advausro_count_query ); } catch (Exception $e) { $this->advausro_logs = []; $this->advausro_total_items = 0; } } public function advausro_get_logs() { return $this->advausro_logs; } public function advausro_get_total_items() { return $this->advausro_total_items; } public function advausro_get_items_per_page() { return $this->advausro_items_per_page; } public function advausro_get_current_page() { return $this->advausro_current_page; } public function advausro_get_all_actions() { global $wpdb; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from the local WordPress prefix. return $wpdb->get_col("SELECT DISTINCT action FROM {$this->advausro_logs_table} ORDER BY action ASC"); } } class ADVAUSRO_Audit_Log_Table extends \WP_List_Table { public function __construct() { parent::__construct([ 'singular' => 'log', 'plural' => 'logs', 'ajax' => false ]); } /** * Get columns - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function get_columns() { return [ 'cb' => '', 'id' => __('ID', 'advanced-use-role-management'), 'action' => __('Action', 'advanced-use-role-management'), 'user_id' => __('User', 'advanced-use-role-management'), 'log_message' => __('Message', 'advanced-use-role-management'), 'created_at' => __('Date', 'advanced-use-role-management') ]; } /** * Get sortable columns - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function get_sortable_columns() { return [ 'id' => ['id', false], 'action' => ['action', true], 'user_id' => ['user_id', true], 'created_at' => ['created_at', true] ]; } /** * Get bulk actions - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function get_bulk_actions() { return [ 'bulk_delete' => __('Delete', 'advanced-use-role-management'), 'bulk_export' => __('Export', 'advanced-use-role-management') ]; } /** * Column default - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function column_default($advausro_item, $advausro_column_name) { switch ($advausro_column_name) { case 'id': return isset($advausro_item['ID']) ? '#' . esc_html($advausro_item['ID']) . '' : esc_html__('N/A', 'advanced-use-role-management'); default: return isset($advausro_item[$advausro_column_name]) ? esc_html($advausro_item[$advausro_column_name]) : ''; } } /** * Column checkbox - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function column_cb($advausro_item) { return sprintf('', esc_attr($advausro_item['ID'])); } /** * Column action - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function column_action($advausro_item) { $advausro_action_labels = [ 'add_role' => __('Add Role', 'advanced-use-role-management'), 'delete_role' => __('Delete Role', 'advanced-use-role-management'), 'add_capability' => __('Add Capability', 'advanced-use-role-management'), 'delete_capability' => __('Delete Capability', 'advanced-use-role-management'), 'clone_role' => __('Clone Role', 'advanced-use-role-management'), 'update_capabilities' => __('Update Capabilities', 'advanced-use-role-management'), 'remove_temp_role' => __('Remove Temporary Role', 'advanced-use-role-management'), 'assign_temp_role' => __('Assign Temporary Role', 'advanced-use-role-management') ]; $advausro_label = $advausro_action_labels[$advausro_item['action']] ?? ucfirst(str_replace('_', ' ', $advausro_item['action'])); $advausro_class = 'advausro-action-badge advausro-action-' . sanitize_html_class($advausro_item['action']); return sprintf('%s', esc_attr($advausro_class), esc_html($advausro_label)); } /** * Column user ID - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function column_user_id($advausro_item) { $advausro_user_id = isset($advausro_item['user_id']) ? absint($advausro_item['user_id']) : 0; if ($advausro_user_id) { $advausro_user = get_userdata($advausro_user_id); if ($advausro_user) { return '' . esc_html($advausro_user->display_name) . ' @' . esc_html($advausro_user->user_login) . ''; } } return esc_html__('Unknown User', 'advanced-use-role-management'); } /** * Column log message - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function column_log_message($advausro_item) { $advausro_message = esc_html($advausro_item['log_message']); if (strlen($advausro_message) > 100) { $advausro_message = substr($advausro_message, 0, 100) . '...'; } return $advausro_message; } /** * Column created at - Inherited from WP_List_Table * Required method for WordPress List Table functionality */ public function column_created_at($advausro_item) { // Use WordPress default date and time format options with fallbacks $date_format = get_option('date_format', 'F j, Y'); $time_format = get_option('time_format', 'g:i a'); // Ensure we have valid format strings if (empty($date_format)) { $date_format = 'F j, Y'; } if (empty($time_format)) { $time_format = 'g:i a'; } // Check if created_at exists and is valid (TIMESTAMP columns can't be empty strings) if (!empty($advausro_item['created_at']) && $advausro_item['created_at'] !== '0000-00-00 00:00:00' && $advausro_item['created_at'] !== null) { $timestamp = strtotime($advausro_item['created_at']); if ($timestamp !== false && $timestamp > 0) { try { $advausro_date = date_i18n($date_format . ' ' . $time_format, $timestamp); if (!empty($advausro_date)) { return esc_html($advausro_date); } } catch (Exception $e) { // Fallback to basic date formatting if date_i18n fails } // Fallback to basic date formatting $advausro_date = gmdate('Y-m-d H:i:s', $timestamp); return esc_html($advausro_date); } } // If we get here, the date is invalid or empty return esc_html__('N/A', 'advanced-use-role-management'); } public function advausro_prepare_items() { global $wpdb; // 🔐 Permission check - ensure user has at least read capability if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to access this page.', 'advanced-use-role-management')); } // 🔐 Nonce verification for AJAX requests if (defined('DOING_AJAX') && DOING_AJAX) { $advausro_action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action'])) : ''; $advausro_nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; // Verify nonce for specific bulk actions if ($advausro_action === 'bulk_export' || $advausro_action === 'bulk_delete') { if (!isset($advausro_nonce) || !wp_verify_nonce($advausro_nonce, 'bulk-logs')) { wp_send_json_error(['message' => esc_html__('Nonce verification failed', 'advanced-use-role-management')]); } } } // 📋 Setup table columns, hidden columns, and sortable columns $advausro_columns = $this->get_columns(); $advausro_hidden = []; $advausro_sortable = $this->get_sortable_columns(); $this->_column_headers = [$advausro_columns, $advausro_hidden, $advausro_sortable]; // 🛡️ Sanitize table name using prefix $advausro_table = $wpdb->prefix . 'advausro_role_management_logs'; // Initialize WHERE clauses and arguments array $advausro_where_clauses = []; $advausro_args = []; // 🔍 Search filter - safely handle search term $advausro_search = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; if (!empty($advausro_search)) { $advausro_like = '%' . $wpdb->esc_like($advausro_search) . '%'; $advausro_where_clauses[] = '(action LIKE %s OR log_message LIKE %s)'; $advausro_args[] = $advausro_like; $advausro_args[] = $advausro_like; } // 📌 Event type filter - sanitize input $advausro_event_type = isset($_GET['event_type']) ? sanitize_text_field(wp_unslash($_GET['event_type'])) : ''; if (!empty($advausro_event_type)) { $advausro_where_clauses[] = 'action = %s'; $advausro_args[] = $advausro_event_type; } // 📅 Date range filter - validate dates $advausro_start_date = isset($_GET['start_date']) ? sanitize_text_field(wp_unslash($_GET['start_date'])) : ''; $advausro_end_date = isset($_GET['end_date']) ? sanitize_text_field(wp_unslash($_GET['end_date'])) : ''; if (!empty($advausro_start_date) && !empty($advausro_end_date)) { // Validate date format if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $advausro_start_date) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $advausro_end_date) ) { $advausro_where_clauses[] = 'DATE(created_at) BETWEEN %s AND %s'; $advausro_args[] = $advausro_start_date; $advausro_args[] = $advausro_end_date; } } // 🧭 Ordering parameters - validate allowed fields and directions $advausro_allowed_orderby = ['created_at', 'action', 'user_id']; $advausro_orderby = isset($_GET['orderby']) ? sanitize_text_field(wp_unslash($_GET['orderby'])) : 'created_at'; $advausro_orderby = in_array($advausro_orderby, $advausro_allowed_orderby, true) ? $advausro_orderby : 'created_at'; $advausro_order = isset($_GET['order']) ? strtoupper(sanitize_text_field(wp_unslash($_GET['order']))) : 'DESC'; $advausro_order = in_array($advausro_order, ['ASC', 'DESC'], true) ? $advausro_order : 'DESC'; // 📊 Count total items with prepared statement $advausro_count_query = "SELECT COUNT(*) FROM {$advausro_table}"; if (!empty($advausro_where_clauses)) { $advausro_count_query .= " WHERE " . implode(' AND ', $advausro_where_clauses); } $advausro_total_items = !empty($advausro_args) ? $wpdb->get_var($wpdb->prepare($advausro_count_query, $advausro_args)) // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Count query is assembled from validated fragments and prepared immediately here. : $wpdb->get_var($advausro_count_query); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- No placeholders are present when filters are empty. // 📄 Pagination configuration $advausro_per_page = 20; $advausro_current_page = $this->get_pagenum(); $advausro_offset = ($advausro_current_page - 1) * $advausro_per_page; // 📥 Fetch paginated data with prepared statement $advausro_select_query = "SELECT id AS ID, action, user_id, log_message, created_at FROM {$advausro_table}"; if (!empty($advausro_where_clauses)) { $advausro_select_query .= " WHERE " . implode(' AND ', $advausro_where_clauses); } // 🔐 SECURITY FIX: Build the complete query with validated parameters to prevent SQL injection $advausro_all_args = $advausro_args; $advausro_all_args[] = $advausro_per_page; $advausro_all_args[] = $advausro_offset; // Use validated orderby and order directly in the query since they're already sanitized $advausro_select_query .= " ORDER BY {$advausro_orderby} {$advausro_order} LIMIT %d OFFSET %d"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query string is prepared immediately here from sanitized parts. $this->items = $wpdb->get_results($wpdb->prepare($advausro_select_query, $advausro_all_args), ARRAY_A); // 🧮 Set pagination arguments $this->set_pagination_args([ 'total_items' => $advausro_total_items, 'per_page' => $advausro_per_page, 'total_pages' => ceil($advausro_total_items / $advausro_per_page), ]); } } function advausro_render_audit_log_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Sorry, you are not allowed to access this page.', 'advanced-use-role-management')); } global $wpdb; $advausro_table_name = $wpdb->prefix . 'advausro_role_management_logs'; $advausro_action_labels = [ 'add_role' => __('Add Role', 'advanced-use-role-management'), 'delete_role' => __('Delete Role', 'advanced-use-role-management'), 'add_capability' => __('Add Capability', 'advanced-use-role-management'), 'delete_capability' => __('Delete Capability', 'advanced-use-role-management'), 'clone_role' => __('Clone Role', 'advanced-use-role-management'), 'update_capabilities' => __('Update Capabilities', 'advanced-use-role-management'), 'remove_temp_role' => __('Remove Temporary Role', 'advanced-use-role-management'), 'assign_temp_role' => __('Assign Temporary Role', 'advanced-use-role-management') ]; // Fixed SQL query - removed prepare() since we're not using placeholders // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is built from the local WordPress prefix. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from the local WordPress prefix. $advausro_event_types = $wpdb->get_col("SELECT DISTINCT action FROM {$advausro_table_name} ORDER BY action ASC"); if (empty($advausro_event_types)) { $advausro_event_types = array_keys($advausro_action_labels); } $advausro_event_types_labeled = []; foreach ($advausro_event_types as $advausro_event) { $advausro_event_types_labeled[$advausro_event] = $advausro_action_labels[$advausro_event] ?? ucfirst(str_replace('_', ' ', $advausro_event)); } $advausro_list_table = new ADVAUSRO_Audit_Log_Table(); ob_start(); // Bulk actions are handled in ADVAUSRO_Connector::advausro_handle_exports() // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only status message displayed after a validated admin redirect. if (isset($_GET['message']) && 'bulk_deleted' === sanitize_text_field(wp_unslash($_GET['message'])) && isset($_GET['deleted'])) { $advausro_deleted_count = absint(wp_unslash($_GET['deleted'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only status count displayed after a validated admin redirect. printf( '
%s