Files
Sharepoint-Toolbox/SharepointToolbox/Services/Export/CsvExportService.cs
T
Dev 12dd1de9f2 chore: release v2.4
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:50:03 +02:00

173 lines
7.1 KiB
C#

using System.IO;
using System.Text;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
namespace SharepointToolbox.Services.Export;
/// <summary>
/// Exports permission entries to CSV format.
/// Ports PowerShell Merge-PermissionRows + Export-Csv functionality.
/// </summary>
public class CsvExportService
{
private static string BuildHeader()
{
var T = TranslationSource.Instance;
return $"\"{T["report.col.object"]}\",\"{T["report.col.title"]}\",\"{T["report.col.url"]}\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\"";
}
/// <summary>
/// Builds a CSV string from the supplied permission entries.
/// Merges rows with identical (Users, PermissionLevels, GrantedThrough) by pipe-joining URLs and Titles.
/// </summary>
public string BuildCsv(IReadOnlyList<PermissionEntry> entries)
{
var sb = new StringBuilder();
sb.AppendLine(BuildHeader());
// Merge: group by (Users, PermissionLevels, GrantedThrough) — port of PS Merge-PermissionRows
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType = g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
/// <summary>
/// Writes the CSV to the specified file path using UTF-8 with BOM (for Excel compatibility).
/// </summary>
public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
/// <summary>
/// Header for simplified CSV export — includes "SimplifiedLabels" and "RiskLevel" columns.
/// </summary>
private static string BuildSimplifiedHeader()
{
var T = TranslationSource.Instance;
return $"\"{T["report.col.object"]}\",\"{T["report.col.title"]}\",\"{T["report.col.url"]}\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"SimplifiedLabels\",\"RiskLevel\",\"GrantedThrough\"";
}
/// <summary>
/// Builds a CSV string from simplified permission entries.
/// Includes SimplifiedLabels and RiskLevel columns after raw Permissions.
/// Uses the same merge logic as the standard BuildCsv.
/// </summary>
public string BuildCsv(IReadOnlyList<SimplifiedPermissionEntry> entries)
{
var sb = new StringBuilder();
sb.AppendLine(BuildSimplifiedHeader());
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType = g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
SimplifiedLabels = g.First().SimplifiedLabels,
RiskLevel = g.First().RiskLevel.ToString(),
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.SimplifiedLabels),
Csv(row.RiskLevel), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
/// <summary>
/// Writes simplified CSV to the specified file path.
/// </summary>
public async Task WriteAsync(IReadOnlyList<SimplifiedPermissionEntry> entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
/// <summary>
/// Writes permission entries with optional per-site partitioning.
/// Single → writes one file at <paramref name="basePath"/>.
/// BySite → one file per site-collection URL, suffixed on the base path.
/// </summary>
public Task WriteAsync(
IReadOnlyList<PermissionEntry> entries,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
entries, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, path, c),
ct);
/// <summary>Simplified-entry split variant.</summary>
public Task WriteAsync(
IReadOnlyList<SimplifiedPermissionEntry> entries,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
entries, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, path, c),
ct);
internal static IEnumerable<(string Label, IReadOnlyList<PermissionEntry> Partition)> PartitionBySite(
IReadOnlyList<PermissionEntry> entries)
{
return entries
.GroupBy(e => ReportSplitHelper.DeriveSiteCollectionUrl(e.Url), StringComparer.OrdinalIgnoreCase)
.Select(g => (
Label: ReportSplitHelper.DeriveSiteLabel(g.Key),
Partition: (IReadOnlyList<PermissionEntry>)g.ToList()));
}
internal static IEnumerable<(string Label, IReadOnlyList<SimplifiedPermissionEntry> Partition)> PartitionBySite(
IReadOnlyList<SimplifiedPermissionEntry> entries)
{
return entries
.GroupBy(e => ReportSplitHelper.DeriveSiteCollectionUrl(e.Url), StringComparer.OrdinalIgnoreCase)
.Select(g => (
Label: ReportSplitHelper.DeriveSiteLabel(g.Key),
Partition: (IReadOnlyList<SimplifiedPermissionEntry>)g.ToList()));
}
/// <summary>RFC 4180 CSV field escaping with formula-injection guard.</summary>
private static string Csv(string value) => CsvSanitizer.Escape(value);
}