using System.IO; using System.Text; using SharepointToolbox.Core.Helpers; using SharepointToolbox.Core.Models; using SharepointToolbox.Localization; namespace SharepointToolbox.Services.Export; /// /// Exports user access audit results to CSV format. /// Produces one CSV file per audited user with a summary section at the top. /// public class UserAccessCsvExportService { private static string BuildDataHeader() { var T = TranslationSource.Instance; return $"\"{T["report.col.site"]}\",\"{T["report.col.object_type"]}\",\"{T["report.col.object"]}\",\"{T["report.col.url"]}\",\"{T["report.col.permission_level"]}\",\"{T["report.col.access_type"]}\",\"{T["report.col.granted_through"]}\""; } /// /// Builds a CSV string for a single user's access entries. /// Includes a summary section at the top followed by data rows. /// public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList entries) { var T = TranslationSource.Instance; var sb = new StringBuilder(); // Summary section var sitesCount = entries.Select(e => e.SiteUrl).Distinct().Count(); var highPrivCount = entries.Count(e => e.IsHighPrivilege); sb.AppendLine($"\"{T["report.title.user_access"]}\""); sb.AppendLine($"\"{T["report.col.user"]}\",\"{Csv(userDisplayName)} ({Csv(userLogin)})\""); sb.AppendLine($"\"{T["report.stat.total_accesses"]}\",\"{entries.Count}\""); sb.AppendLine($"\"{T["report.col.sites"]}\",\"{sitesCount}\""); sb.AppendLine($"\"{T["report.stat.high_privilege"]}\",\"{highPrivCount}\""); sb.AppendLine($"\"{T["report.text.generated"]}\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); sb.AppendLine(); // Blank line separating summary from data // Data rows sb.AppendLine(BuildDataHeader()); foreach (var entry in entries) { sb.AppendLine(string.Join(",", new[] { Csv(entry.SiteTitle), Csv(entry.ObjectType), Csv(entry.ObjectTitle), Csv(entry.ObjectUrl), Csv(entry.PermissionLevel), Csv(entry.AccessType.ToString()), Csv(entry.GrantedThrough) })); } return sb.ToString(); } /// /// Writes one CSV file per user to the specified directory. /// File names: audit_{email}_{date}.csv /// public async Task WriteAsync( IReadOnlyList allEntries, string directoryPath, CancellationToken ct) { Directory.CreateDirectory(directoryPath); var dateStr = DateTime.Now.ToString("yyyy-MM-dd"); // Group by user var byUser = allEntries.GroupBy(e => e.UserLogin); foreach (var group in byUser) { ct.ThrowIfCancellationRequested(); var userLogin = group.Key; var displayName = group.First().UserDisplayName; var entries = group.ToList(); // Sanitize email for filename (replace @ and other invalid chars) var safeLogin = SanitizeFileName(userLogin); var fileName = $"audit_{safeLogin}_{dateStr}.csv"; var filePath = Path.Combine(directoryPath, fileName); var csv = BuildCsv(displayName, userLogin, entries); await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } } /// /// Writes all entries to a single CSV file (alternative for single-file export). /// Used when the ViewModel export command picks a single file path. /// When is true, entries are consolidated using /// and written in a compact multi-location format. /// public async Task WriteSingleFileAsync( IReadOnlyList entries, string filePath, CancellationToken ct, bool mergePermissions = false) { var T = TranslationSource.Instance; if (mergePermissions) { var consolidated = PermissionConsolidator.Consolidate(entries); var sb = new StringBuilder(); // Summary section sb.AppendLine($"\"{T["report.title.user_access_consolidated"]}\""); sb.AppendLine($"\"{T["report.stat.users_audited"]}\",\"{consolidated.Select(e => e.UserLogin).Distinct().Count()}\""); sb.AppendLine($"\"{T["report.stat.total_entries"]}\",\"{consolidated.Count}\""); sb.AppendLine($"\"{T["report.text.generated"]}\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); sb.AppendLine(); // Header sb.AppendLine($"\"{T["report.col.user"]}\",\"User Login\",\"{T["report.col.permission_level"]}\",\"{T["report.col.access_type"]}\",\"{T["report.col.granted_through"]}\",\"Locations\",\"Location Count\""); // Data rows foreach (var entry in consolidated) { var locations = string.Join("; ", entry.Locations.Select(l => l.SiteTitle)); sb.AppendLine(string.Join(",", new[] { $"\"{entry.UserDisplayName}\"", $"\"{entry.UserLogin}\"", $"\"{entry.PermissionLevel}\"", $"\"{entry.AccessType}\"", $"\"{entry.GrantedThrough}\"", $"\"{locations}\"", $"\"{entry.LocationCount}\"" })); } await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(false), ct); return; } { var sb = new StringBuilder(); var fullHeader = $"\"{T["report.col.user"]}\",\"User Login\"," + BuildDataHeader(); // Summary var users = entries.Select(e => e.UserLogin).Distinct().ToList(); sb.AppendLine($"\"{T["report.title.user_access"]}\""); sb.AppendLine($"\"{T["report.stat.users_audited"]}\",\"{users.Count}\""); sb.AppendLine($"\"{T["report.stat.total_accesses"]}\",\"{entries.Count}\""); sb.AppendLine($"\"{T["report.text.generated"]}\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); sb.AppendLine(); sb.AppendLine(fullHeader); foreach (var entry in entries) { sb.AppendLine(string.Join(",", new[] { Csv(entry.UserDisplayName), Csv(entry.UserLogin), Csv(entry.SiteTitle), Csv(entry.ObjectType), Csv(entry.ObjectTitle), Csv(entry.ObjectUrl), Csv(entry.PermissionLevel), Csv(entry.AccessType.ToString()), Csv(entry.GrantedThrough) })); } await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } } /// RFC 4180 CSV field escaping: wrap in double quotes, double internal quotes. private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return "\"\""; return $"\"{value.Replace("\"", "\"\"")}\""; } private static string SanitizeFileName(string name) { var invalid = Path.GetInvalidFileNameChars(); var sb = new StringBuilder(name.Length); foreach (var c in name) sb.Append(invalid.Contains(c) ? '_' : c); return sb.ToString(); } }