admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'abilities_bridge_activity_log' ), ) ); } /** * Verify the page navigation nonce. * * Returns true if nonce is valid or not present (first page load). * Dies with error if nonce is present but invalid. * * @return bool True if request is valid. */ private static function verify_page_nonce() { $nonce = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); // No nonce present = first page load, allow with defaults. if ( empty( $nonce ) ) { return false; } // Nonce present - verify it. if ( ! wp_verify_nonce( $nonce, 'abilities_bridge_activity_log_nav' ) ) { wp_die( esc_html__( 'Security verification failed. Please use the navigation links provided.', 'abilities-bridge' ), esc_html__( 'Security Check Failed', 'abilities-bridge' ), array( 'response' => 403 ) ); } return true; } /** * Get a sanitized GET parameter for display purposes. * * Verifies nonce before accessing parameters. If no nonce is present * (first page load), returns the default value. * * @param string $key Parameter key. * @param string $default Default value. * @return string Sanitized value. */ private static function get_display_param( $key, $default = '' ) { // Verify nonce - returns false if no nonce (first load). if ( ! self::verify_page_nonce() ) { return $default; } $value = filter_input( INPUT_GET, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS ); return ! empty( $value ) ? sanitize_key( $value ) : $default; } /** * Get a sanitized integer GET parameter for display purposes. * * Verifies nonce before accessing parameters. If no nonce is present * (first page load), returns the default value. * * @param string $key Parameter key. * @param int $default Default value. * @return int Sanitized integer value. */ private static function get_display_int_param( $key, $default = 0 ) { // Verify nonce - returns false if no nonce (first load). if ( ! self::verify_page_nonce() ) { return $default; } $value = filter_input( INPUT_GET, $key, FILTER_VALIDATE_INT ); return false !== $value && null !== $value ? max( 1, absint( $value ) ) : $default; } /** * Render the page */ public static function render_page() { $current_tab = self::get_display_param( 'tab', 'activity' ); ?>

403 ) ); } // Nonce is valid - build filters safely using filter_input. // User ID filter. $user_id = filter_input( INPUT_GET, 'user_id', FILTER_VALIDATE_INT ); if ( false !== $user_id && null !== $user_id && $user_id > 0 ) { $filters['user_id'] = $user_id; } // Status filter. $status = filter_input( INPUT_GET, 'status', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( ! empty( $status ) ) { $filters['status'] = sanitize_key( $status ); } // Search term filter. $search = filter_input( INPUT_GET, 'search', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( ! empty( $search ) ) { $filters['search'] = sanitize_text_field( $search ); } return $filters; } /** * Render Activity Log tab */ private static function render_activity_tab() { // Get validated filters (handles all nonce checking internally). $filters = self::get_validated_filters(); // Get filter options. $users = get_users( array( 'fields' => array( 'ID', 'user_login' ) ) ); ?>
1 ) : ?>
add_query_arg( 'paged', '%#%', $base_url ), 'format' => '', 'current' => $page, 'total' => $result['total_pages'], 'prev_text' => '«', 'next_text' => '»', ) ) ); ?>
MCP Chat ✗ ✓ 1 ) : ?>
' . esc_html( $i ) . ' '; } else { echo '' . esc_html( $i ) . ' '; } } ?>

= 0 && $new_retention <= 90 ) { update_option( 'abilities_bridge_log_retention_days', $new_retention ); $retention_days = $new_retention; echo '

' . esc_html__( 'Log retention settings saved.', 'abilities-bridge' ) . '

'; } } // Handle manual cleanup. if ( isset( $_POST['abilities_bridge_manual_cleanup'] ) && check_admin_referer( 'abilities_bridge_cleanup_nonce' ) ) { $results = Abilities_Bridge_Log_Cleanup::manual_cleanup(); echo '

'; printf( /* translators: 1: number of logs deleted, 2: number of conversations purged */ esc_html__( 'Cleanup complete: %1$d logs deleted, %2$d conversations purged.', 'abilities-bridge' ), absint( $results['logs_deleted'] ), absint( $results['conversations_deleted'] ) ); echo '

'; // Refresh stats. $stats = Abilities_Bridge_Database::get_log_statistics(); $last_cleanup = Abilities_Bridge_Log_Cleanup::get_last_cleanup_info(); } ?>

()

'Unauthorized' ) ); } // Extract and validate filters from POST (AJAX request). $filters = array(); $page = 1; // Retrieve filters from POST data (nonce verified above via check_ajax_referer). $raw_filters = filter_input( INPUT_POST, 'filters', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); if ( ! empty( $raw_filters ) && is_array( $raw_filters ) ) { $posted_filters = map_deep( $raw_filters, 'sanitize_text_field' ); if ( ! empty( $posted_filters['user_id'] ) ) { $filters['user_id'] = intval( $posted_filters['user_id'] ); } if ( ! empty( $posted_filters['status'] ) ) { $filters['status'] = sanitize_key( $posted_filters['status'] ); } if ( ! empty( $posted_filters['search'] ) ) { $filters['search'] = sanitize_text_field( $posted_filters['search'] ); } if ( ! empty( $posted_filters['page'] ) ) { $page = intval( $posted_filters['page'] ); } } $page = max( 1, $page ); $per_page = 50; // Get logs with filters. $result = Abilities_Bridge_Database::get_logs_filtered( $filters, $page, $per_page ); // Render table rows only (not entire table). ob_start(); self::render_table_rows( $result ); $html = ob_get_clean(); // Render pagination. ob_start(); self::render_pagination( $result, $page ); $pagination = ob_get_clean(); wp_send_json_success( array( 'html' => $html, 'pagination' => $pagination, ) ); } /** * AJAX: Export logs to CSV */ public static function ajax_export_logs() { check_ajax_referer( 'abilities_bridge_activity_log', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Unauthorized' ); } // Rate limiting - prevent export spam/abuse. // Max 5 exports per minute per user. $user_id = get_current_user_id(); $transient_key = 'abilities_bridge_export_attempts_' . $user_id; $attempts = get_transient( $transient_key ); if ( false === $attempts ) { $attempts = 0; } if ( $attempts >= 5 ) { wp_die( esc_html__( 'Too many export attempts. Please wait a moment and try again.', 'abilities-bridge' ), esc_html__( 'Rate Limit Exceeded', 'abilities-bridge' ), array( 'response' => 429 ) ); } // Increment export attempts counter (expires in 60 seconds). set_transient( $transient_key, $attempts + 1, MINUTE_IN_SECONDS ); // Build filters from POST data (nonce verified above). $filters = array(); if ( ! empty( $_POST['user_id'] ) ) { $filters['user_id'] = intval( wp_unslash( $_POST['user_id'] ) ); } if ( ! empty( $_POST['status'] ) ) { $filters['status'] = sanitize_key( wp_unslash( $_POST['status'] ) ); } if ( ! empty( $_POST['search'] ) ) { $filters['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) ); } // Get logs (max 10000). $result = Abilities_Bridge_Database::get_logs_filtered( $filters, 1, 10000 ); // Set headers for CSV download. header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=activity-log-' . gmdate( 'Y-m-d-His' ) . '.csv' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); // Output CSV using PHP output stream (standard WordPress CSV export pattern). $output = fopen( 'php://output', 'w' ); // Add UTF-8 BOM for Excel compatibility. fwrite( $output, "\xEF\xBB\xBF" ); // CSV headers. fputcsv( $output, array( 'Timestamp', 'User', 'Conversation', 'Function', 'Status', 'Action', 'Error' ) ); // CSV rows. foreach ( $result['items'] as $log ) { fputcsv( $output, array( $log['created_at'], $log['username'], $log['conversation_title'] ? $log['conversation_title'] : 'N/A', $log['function_name'], $log['error_message'] ? 'Error' : 'Success', $log['action'], $log['error_message'] ? $log['error_message'] : '', ) ); } fclose( $output ); exit; } /** * AJAX: Restore conversation */ public static function ajax_restore_conversation() { check_ajax_referer( 'abilities_bridge_activity_log', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( ! $conversation_id ) { wp_send_json_error( array( 'message' => 'Invalid conversation ID' ) ); } if ( ! self::get_accessible_conversation( $conversation_id, true ) ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $result = Abilities_Bridge_Database::restore_conversation( $conversation_id, get_current_user_id() ); if ( $result ) { wp_send_json_success( array( 'message' => 'Conversation restored successfully' ) ); } else { wp_send_json_error( array( 'message' => 'Failed to restore conversation' ) ); } } /** * AJAX: Permanently delete conversation */ public static function ajax_permanently_delete() { check_ajax_referer( 'abilities_bridge_activity_log', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( ! $conversation_id ) { wp_send_json_error( array( 'message' => 'Invalid conversation ID' ) ); } if ( ! self::get_accessible_conversation( $conversation_id, true ) ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $result = Abilities_Bridge_Database::permanently_delete_conversation( $conversation_id, get_current_user_id() ); if ( $result ) { wp_send_json_success( array( 'message' => 'Conversation permanently deleted' ) ); } else { wp_send_json_error( array( 'message' => 'Failed to delete conversation' ) ); } } }