using System.IO; using System.Text; using SharepointToolbox.Core.Models; using SharepointToolbox.Localization; using static SharepointToolbox.Services.Export.PermissionHtmlFragments; namespace SharepointToolbox.Services.Export; /// /// Exports permission entries to a self-contained interactive HTML report. /// Ports PowerShell Export-PermissionsToHTML functionality. /// No external CSS/JS dependencies — everything is inline so the file can be /// emailed or served from any static host. The standard and simplified /// variants share their document shell, stats cards, CSS, pill rendering, and /// inline script via ; this class only /// owns the table column sets and the simplified risk summary. /// public class HtmlExportService { /// /// Builds a self-contained HTML string from the supplied permission /// entries. Standard report: columns are Object / Title / URL / Unique / /// Users / Permission / Granted Through. When /// is provided, SharePoint group pills /// become expandable rows listing resolved members. /// public string BuildHtml( IReadOnlyList entries, ReportBranding? branding = null, IReadOnlyDictionary>? groupMembers = null) { var T = TranslationSource.Instance; var (totalEntries, uniquePermSets, distinctUsers) = ComputeStats( entries.Count, entries.Select(e => e.PermissionLevels), entries.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))); var sb = new StringBuilder(); AppendHead(sb, T["report.title.permissions"], includeRiskCss: false); sb.AppendLine(""); sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); sb.AppendLine($"

{T["report.title.permissions"]}

"); AppendStatsCards(sb, totalEntries, uniquePermSets, distinctUsers); AppendFilterInput(sb); AppendTableOpen(sb); sb.AppendLine(""); sb.AppendLine($" {T["report.col.object"]}{T["report.col.title"]}{T["report.col.url"]}{T["report.badge.unique"]}{T["report.col.users_groups"]}{T["report.col.permission_level"]}{T["report.col.granted_through"]}"); sb.AppendLine(""); sb.AppendLine(""); int grpMemIdx = 0; foreach (var entry in entries) { var typeCss = ObjectTypeCss(entry.ObjectType); var uniqueCss = entry.HasUniquePermissions ? "badge unique" : "badge inherited"; var uniqueLbl = entry.HasUniquePermissions ? T["report.badge.unique"] : T["report.badge.inherited"]; var (pills, subRows) = BuildUserPillsCell( entry.UserLogins, entry.Users, entry.PrincipalType, groupMembers, colSpan: 7, grpMemIdx: ref grpMemIdx); sb.AppendLine(""); sb.AppendLine($" {HtmlEncode(entry.ObjectType)}"); sb.AppendLine($" {HtmlEncode(entry.Title)}"); sb.AppendLine($" {T["report.text.link"]}"); sb.AppendLine($" {uniqueLbl}"); sb.AppendLine($" {pills}"); sb.AppendLine($" {HtmlEncode(entry.PermissionLevels)}"); sb.AppendLine($" {HtmlEncode(entry.GrantedThrough)}"); sb.AppendLine(""); if (subRows.Length > 0) sb.Append(subRows); } AppendTableClose(sb); AppendInlineJs(sb); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } /// /// Builds a self-contained HTML string from simplified permission entries. /// Adds a risk-level summary card strip plus two columns (Simplified, /// Risk) relative to . /// Color-coded risk badges use . /// public string BuildHtml( IReadOnlyList entries, ReportBranding? branding = null, IReadOnlyDictionary>? groupMembers = null) { var T = TranslationSource.Instance; var summaries = PermissionSummaryBuilder.Build(entries); var (totalEntries, uniquePermSets, distinctUsers) = ComputeStats( entries.Count, entries.Select(e => e.PermissionLevels), entries.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))); var sb = new StringBuilder(); AppendHead(sb, T["report.title.permissions_simplified"], includeRiskCss: true); sb.AppendLine(""); sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); sb.AppendLine($"

{T["report.title.permissions_simplified"]}

"); AppendStatsCards(sb, totalEntries, uniquePermSets, distinctUsers); sb.AppendLine("
"); foreach (var s in summaries) { var (bg, text, border) = RiskLevelColors(s.RiskLevel); sb.AppendLine($"
"); sb.AppendLine($"
{s.Count}
"); sb.AppendLine($"
{HtmlEncode(s.Label)}
"); sb.AppendLine($"
{s.DistinctUsers} {T["report.text.users_parens"]}
"); sb.AppendLine("
"); } sb.AppendLine("
"); AppendFilterInput(sb); AppendTableOpen(sb); sb.AppendLine(""); sb.AppendLine($" {T["report.col.object"]}{T["report.col.title"]}{T["report.col.url"]}{T["report.badge.unique"]}{T["report.col.users_groups"]}{T["report.col.permission_level"]}{T["report.col.simplified"]}{T["report.col.risk"]}{T["report.col.granted_through"]}"); sb.AppendLine(""); sb.AppendLine(""); int grpMemIdx = 0; foreach (var entry in entries) { var typeCss = ObjectTypeCss(entry.ObjectType); var uniqueCss = entry.HasUniquePermissions ? "badge unique" : "badge inherited"; var uniqueLbl = entry.HasUniquePermissions ? T["report.badge.unique"] : T["report.badge.inherited"]; var (riskBg, riskText, riskBorder) = RiskLevelColors(entry.RiskLevel); var (pills, subRows) = BuildUserPillsCell( entry.UserLogins, entry.Inner.Users, entry.Inner.PrincipalType, groupMembers, colSpan: 9, grpMemIdx: ref grpMemIdx); sb.AppendLine(""); sb.AppendLine($" {HtmlEncode(entry.ObjectType)}"); sb.AppendLine($" {HtmlEncode(entry.Title)}"); sb.AppendLine($" {T["report.text.link"]}"); sb.AppendLine($" {uniqueLbl}"); sb.AppendLine($" {pills}"); sb.AppendLine($" {HtmlEncode(entry.PermissionLevels)}"); sb.AppendLine($" {HtmlEncode(entry.SimplifiedLabels)}"); sb.AppendLine($" {HtmlEncode(entry.RiskLevel.ToString())}"); sb.AppendLine($" {HtmlEncode(entry.GrantedThrough)}"); sb.AppendLine(""); if (subRows.Length > 0) sb.Append(subRows); } AppendTableClose(sb); AppendInlineJs(sb); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } /// Writes the HTML report to the specified file path using UTF-8 without BOM. public async Task WriteAsync( IReadOnlyList entries, string filePath, CancellationToken ct, ReportBranding? branding = null, IReadOnlyDictionary>? groupMembers = null) { var html = BuildHtml(entries, branding, groupMembers); await ExportFileWriter.WriteHtmlAsync(filePath, html, ct); } /// Writes the simplified HTML report to the specified file path using UTF-8 without BOM. public async Task WriteAsync( IReadOnlyList entries, string filePath, CancellationToken ct, ReportBranding? branding = null, IReadOnlyDictionary>? groupMembers = null) { var html = BuildHtml(entries, branding, groupMembers); await ExportFileWriter.WriteHtmlAsync(filePath, html, ct); } /// /// Split-aware write for permission entries. /// Single → one file. BySite + SeparateFiles → one file per site. /// BySite + SingleTabbed → one file with per-site iframe tabs. /// public async Task WriteAsync( IReadOnlyList entries, string basePath, ReportSplitMode splitMode, HtmlSplitLayout layout, CancellationToken ct, ReportBranding? branding = null, IReadOnlyDictionary>? groupMembers = null) { if (splitMode != ReportSplitMode.BySite) { await WriteAsync(entries, basePath, ct, branding, groupMembers); return; } var partitions = CsvExportService.PartitionBySite(entries).ToList(); if (layout == HtmlSplitLayout.SingleTabbed) { var parts = partitions .Select(p => (p.Label, Html: BuildHtml(p.Partition, branding, groupMembers))) .ToList(); var title = TranslationSource.Instance["report.title.permissions"]; var tabbed = ReportSplitHelper.BuildTabbedHtml(parts, title); await ExportFileWriter.WriteHtmlAsync(basePath, tabbed, ct); return; } foreach (var (label, partEntries) in partitions) { ct.ThrowIfCancellationRequested(); var path = ReportSplitHelper.BuildPartitionPath(basePath, label); await WriteAsync(partEntries, path, ct, branding, groupMembers); } } /// Simplified-entry split variant of . public async Task WriteAsync( IReadOnlyList entries, string basePath, ReportSplitMode splitMode, HtmlSplitLayout layout, CancellationToken ct, ReportBranding? branding = null, IReadOnlyDictionary>? groupMembers = null) { if (splitMode != ReportSplitMode.BySite) { await WriteAsync(entries, basePath, ct, branding, groupMembers); return; } var partitions = CsvExportService.PartitionBySite(entries).ToList(); if (layout == HtmlSplitLayout.SingleTabbed) { var parts = partitions .Select(p => (p.Label, Html: BuildHtml(p.Partition, branding, groupMembers))) .ToList(); var title = TranslationSource.Instance["report.title.permissions_simplified"]; var tabbed = ReportSplitHelper.BuildTabbedHtml(parts, title); await ExportFileWriter.WriteHtmlAsync(basePath, tabbed, ct); return; } foreach (var (label, partEntries) in partitions) { ct.ThrowIfCancellationRequested(); var path = ReportSplitHelper.BuildPartitionPath(basePath, label); await WriteAsync(partEntries, path, ct, branding, groupMembers); } } private static (int total, int uniquePerms, int distinctUsers) ComputeStats( int totalEntries, IEnumerable permissionLevels, IEnumerable userLogins) { var uniquePermSets = permissionLevels.Distinct().Count(); var distinctUsers = userLogins .Select(u => u.Trim()) .Where(u => u.Length > 0) .Distinct() .Count(); return (totalEntries, uniquePermSets, distinctUsers); } private static void AppendTableOpen(StringBuilder sb) { sb.AppendLine("
"); sb.AppendLine(""); } private static void AppendTableClose(StringBuilder sb) { sb.AppendLine(""); sb.AppendLine("
"); sb.AppendLine("
"); } /// Returns inline CSS background, text, and border colors for a risk level. private static (string bg, string text, string border) RiskLevelColors(RiskLevel level) => level switch { RiskLevel.High => ("#FEE2E2", "#991B1B", "#FECACA"), RiskLevel.Medium => ("#FEF3C7", "#92400E", "#FDE68A"), RiskLevel.Low => ("#D1FAE5", "#065F46", "#A7F3D0"), RiskLevel.ReadOnly => ("#DBEAFE", "#1E40AF", "#BFDBFE"), _ => ("#F3F4F6", "#374151", "#E5E7EB") }; }