prefix . self::TABLE_NAME; $charset_collate = $wpdb->get_charset_collate(); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name and charset are safe internal values. $sql = "CREATE TABLE {$table_name} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, method VARCHAR(100) NOT NULL DEFAULT '', tool_name VARCHAR(100) NOT NULL DEFAULT '', request LONGTEXT NOT NULL, response LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'success', PRIMARY KEY (id), KEY created_at (created_at), KEY method (method), KEY user_id (user_id) ) {$charset_collate};"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange -- Custom logging table created on plugin activation via dbDelta(). dbDelta( $sql ); update_option( self::DB_VERSION_KEY, self::DB_VERSION ); } /** * Schedule the daily cleanup cron event. * * @since 1.0.0 * * @return void */ public static function schedule_cleanup(): void { if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_event( time(), 'daily', self::CRON_HOOK ); } } /** * Unschedule the cleanup cron event. * * @since 1.0.0 * * @return void */ public static function unschedule_cleanup(): void { $timestamp = wp_next_scheduled( self::CRON_HOOK ); if ( $timestamp ) { wp_unschedule_event( $timestamp, self::CRON_HOOK ); } } /** * Check if logging is enabled. * * @since 1.0.0 * * @return bool */ public function is_enabled(): bool { $enabled = get_option( 'wlconduit_logging_enabled', '1' ) === '1'; /** * Filter to override the logging setting programmatically. * * Takes priority over the admin setting. * * @since 1.0.0 * * @param bool $enabled Whether logging is enabled. */ return (bool) apply_filters( 'wlconduit_mcp_logging_enabled', $enabled ); } /** * Log an MCP request and response. * * @since 1.0.0 * * @param string $method The JSON-RPC method name. * @param string $tool_name The tool name if applicable. * @param array $request The full request payload. * @param array $response The full response payload. * @param string $status Either 'success' or 'error'. * @return void */ public function log( string $method, string $tool_name, array $request, array $response, string $status ): void { if ( ! $this->is_enabled() ) { return; } global $wpdb; $table_name = $wpdb->prefix . self::TABLE_NAME; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table, no WP API available. $wpdb->insert( $table_name, array( 'user_id' => get_current_user_id(), 'method' => sanitize_text_field( $method ), 'tool_name' => sanitize_text_field( $tool_name ), 'request' => wp_json_encode( $request ), 'response' => wp_json_encode( $response ), 'status' => sanitize_text_field( $status ), ), array( '%d', '%s', '%s', '%s', '%s', '%s' ) ); } /** * Delete logs older than the configured retention period. * * @since 1.0.0 * * @return void */ public function cleanup(): void { global $wpdb; $retention_days = absint( get_option( 'wlconduit_log_retention_days', 30 ) ); if ( 0 === $retention_days ) { return; } $table_name = $wpdb->prefix . self::TABLE_NAME; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, no WP API available. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)', $table_name, $retention_days ) ); } /** * Drop the logs table. Used on uninstall. * * @since 1.0.0 * * @return void */ public static function drop_table(): void { global $wpdb; $table_name = $wpdb->prefix . self::TABLE_NAME; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Custom logs table dropped on plugin uninstall; no WP API exists for dropping a plugin-owned table. $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table_name ) ); delete_option( self::DB_VERSION_KEY ); } }