prefix . 'wbc_logs'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, level varchar(20) NOT NULL, context varchar(100) NOT NULL, message text NOT NULL, data longtext, PRIMARY KEY (id), KEY level (level), KEY context (context), KEY timestamp (timestamp) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); } /** * Get the table name * * @return string */ private static function get_table_name() { if ( self::$table_name === null ) { global $wpdb; self::$table_name = $wpdb->prefix . 'wbc_logs'; } return self::$table_name; } /** * Log a debug message * * @param string $context The context/source of the log. * @param string $message The log message. * @param mixed $data Optional. Additional data to log. */ public static function debug( $context, $message, $data = null ) { self::log( self::DEBUG, $context, $message, $data ); } /** * Log an info message * * @param string $context The context/source of the log. * @param string $message The log message. * @param mixed $data Optional. Additional data to log. */ public static function info( $context, $message, $data = null ) { self::log( self::INFO, $context, $message, $data ); } /** * Log a warning message * * @param string $context The context/source of the log. * @param string $message The log message. * @param mixed $data Optional. Additional data to log. */ public static function warning( $context, $message, $data = null ) { self::log( self::WARNING, $context, $message, $data ); } /** * Log an error message * * @param string $context The context/source of the log. * @param string $message The log message. * @param mixed $data Optional. Additional data to log. */ public static function error( $context, $message, $data = null ) { self::log( self::ERROR, $context, $message, $data ); } /** * Log a message to the database * * @param string $level The log level. * @param string $context The context/source of the log. * @param string $message The log message. * @param mixed $data Optional. Additional data to log. */ private static function log( $level, $context, $message, $data = null ) { global $wpdb; $table_name = self::get_table_name(); // Prepare data for storage $data_json = null; if ( $data !== null ) { $data_json = wp_json_encode( $data, JSON_PRETTY_PRINT ); } // Insert log entry $wpdb->insert( $table_name, array( 'timestamp' => current_time( 'mysql' ), 'level' => $level, 'context' => sanitize_text_field( $context ), 'message' => sanitize_textarea_field( $message ), 'data' => $data_json, ), array( '%s', '%s', '%s', '%s', '%s' ) ); // Also log to WooCommerce logger if available if ( function_exists( 'wc_get_logger' ) ) { $wc_logger = wc_get_logger(); $log_level = strtolower( $level ); if ( method_exists( $wc_logger, $log_level ) ) { $wc_message = "[$context] $message"; if ( $data !== null ) { $wc_message .= ' | Data: ' . wp_json_encode( $data ); } $wc_logger->$log_level( $wc_message, array( 'source' => 'woo-business-central' ) ); } } } /** * Get log entries * * @param array $args Query arguments. * @return array Array of log entries. */ public static function get_logs( $args = array() ) { global $wpdb; $defaults = array( 'level' => '', 'context' => '', 'limit' => 100, 'offset' => 0, 'orderby' => 'timestamp', 'order' => 'DESC', ); $args = wp_parse_args( $args, $defaults ); $table_name = self::get_table_name(); // Build WHERE clause $where = array( '1=1' ); $values = array(); if ( ! empty( $args['level'] ) ) { $where[] = 'level = %s'; $values[] = $args['level']; } if ( ! empty( $args['context'] ) ) { $where[] = 'context = %s'; $values[] = $args['context']; } $where_clause = implode( ' AND ', $where ); // Sanitize orderby and order $allowed_orderby = array( 'id', 'timestamp', 'level', 'context' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'timestamp'; $order = strtoupper( $args['order'] ) === 'ASC' ? 'ASC' : 'DESC'; // Build query $limit = absint( $args['limit'] ); $offset = absint( $args['offset'] ); $sql = "SELECT * FROM $table_name WHERE $where_clause ORDER BY $orderby $order LIMIT $limit OFFSET $offset"; if ( ! empty( $values ) ) { $sql = $wpdb->prepare( $sql, $values ); } return $wpdb->get_results( $sql, ARRAY_A ); } /** * Get total log count * * @param array $args Query arguments. * @return int Total count. */ public static function get_log_count( $args = array() ) { global $wpdb; $table_name = self::get_table_name(); // Build WHERE clause $where = array( '1=1' ); $values = array(); if ( ! empty( $args['level'] ) ) { $where[] = 'level = %s'; $values[] = $args['level']; } if ( ! empty( $args['context'] ) ) { $where[] = 'context = %s'; $values[] = $args['context']; } $where_clause = implode( ' AND ', $where ); $sql = "SELECT COUNT(*) FROM $table_name WHERE $where_clause"; if ( ! empty( $values ) ) { $sql = $wpdb->prepare( $sql, $values ); } return (int) $wpdb->get_var( $sql ); } /** * Clear all logs * * @return bool|int Number of rows deleted or false on error. */ public static function clear_logs() { global $wpdb; $table_name = self::get_table_name(); return $wpdb->query( "TRUNCATE TABLE $table_name" ); } /** * Delete old logs * * @param int $days Number of days to keep logs. * @return int Number of rows deleted. */ public static function cleanup_old_logs( $days = 30 ) { global $wpdb; $table_name = self::get_table_name(); $cutoff_date = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) ); return $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE timestamp < %s", $cutoff_date ) ); } /** * Export logs to CSV format * * @param array $args Query arguments. * @return string CSV content. */ public static function export_to_csv( $args = array() ) { $logs = self::get_logs( array_merge( $args, array( 'limit' => 10000 ) ) ); $output = "ID,Timestamp,Level,Context,Message,Data\n"; foreach ( $logs as $log ) { $row = array( $log['id'], $log['timestamp'], $log['level'], self::sanitize_csv_field( $log['context'] ), self::sanitize_csv_field( $log['message'] ), self::sanitize_csv_field( $log['data'] ?? '' ), ); $output .= implode( ',', $row ) . "\n"; } return $output; } /** * Sanitize a field value for CSV export to prevent formula injection * * @param string $value The value to sanitize. * @return string Sanitized and quoted CSV value. */ private static function sanitize_csv_field( $value ) { // Prevent formula injection by prefixing dangerous characters with a single quote if ( preg_match( '/^[=+\-@\t\r]/', $value ) ) { $value = "'" . $value; } return '"' . str_replace( '"', '""', $value ) . '"'; } }