diff --git a/SharepointToolbox/Services/Export/HtmlExportService.cs b/SharepointToolbox/Services/Export/HtmlExportService.cs index c063e06..eb5e14f 100644 --- a/SharepointToolbox/Services/Export/HtmlExportService.cs +++ b/SharepointToolbox/Services/Export/HtmlExportService.cs @@ -1,18 +1,178 @@ +using System.IO; +using System.Text; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services.Export; /// -/// Exports permission entries to HTML format. -/// Full implementation will be added in Plan 03. +/// Exports permission entries to a self-contained interactive HTML report. +/// Ports PowerShell Export-PermissionsToHTML functionality. +/// No external CSS/JS dependencies — everything is inline. /// public class HtmlExportService { - /// Builds an HTML string from the supplied permission entries. - public string BuildHtml(IReadOnlyList entries) => - throw new NotImplementedException("HtmlExportService.BuildHtml — implemented in Plan 03"); + /// + /// Builds a self-contained HTML string from the supplied permission entries. + /// Includes inline CSS, inline JS filter, stats cards, type badges, unique/inherited badges, and user pills. + /// + public string BuildHtml(IReadOnlyList entries) + { + // Compute stats + var totalEntries = entries.Count; + var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count(); + var distinctUsers = entries + .SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries)) + .Select(u => u.Trim()) + .Where(u => u.Length > 0) + .Distinct() + .Count(); - /// Writes the HTML output to a file. - public Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct) => - throw new NotImplementedException("HtmlExportService.WriteAsync — implemented in Plan 03"); + var sb = new StringBuilder(); + + // ── HTML HEAD ────────────────────────────────────────────────────────── + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("SharePoint Permissions Report"); + sb.AppendLine(""); + sb.AppendLine(""); + + // ── BODY ─────────────────────────────────────────────────────────────── + sb.AppendLine(""); + sb.AppendLine("

SharePoint Permissions Report

"); + + // Stats cards + sb.AppendLine("
"); + sb.AppendLine($"
{totalEntries}
Total Entries
"); + sb.AppendLine($"
{uniquePermSets}
Unique Permission Sets
"); + sb.AppendLine($"
{distinctUsers}
Distinct Users/Groups
"); + sb.AppendLine("
"); + + // Filter input + sb.AppendLine("
"); + sb.AppendLine(" "); + sb.AppendLine("
"); + + // Table + sb.AppendLine("
"); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(" "); + sb.AppendLine(""); + sb.AppendLine(""); + + foreach (var entry in entries) + { + var typeCss = ObjectTypeCss(entry.ObjectType); + var uniqueCss = entry.HasUniquePermissions ? "badge unique" : "badge inherited"; + var uniqueLbl = entry.HasUniquePermissions ? "Unique" : "Inherited"; + + // Build user pills: zip UserLogins and Users (both semicolon-delimited) + var logins = entry.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries); + var names = entry.Users.Split(';', StringSplitOptions.RemoveEmptyEntries); + var pillsBuilder = new StringBuilder(); + for (int i = 0; i < logins.Length; i++) + { + var login = logins[i].Trim(); + var name = i < names.Length ? names[i].Trim() : login; + var isExt = login.Contains("#EXT#", StringComparison.OrdinalIgnoreCase); + var pillCss = isExt ? "user-pill external-user" : "user-pill"; + pillsBuilder.Append($"{HtmlEncode(name)}"); + } + + sb.AppendLine(""); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine(""); + } + + sb.AppendLine(""); + sb.AppendLine("
ObjectTitleURLUniqueUsers/GroupsPermission LevelGranted Through
{HtmlEncode(entry.ObjectType)}{HtmlEncode(entry.Title)}Link{uniqueLbl}{pillsBuilder}{HtmlEncode(entry.PermissionLevels)}{HtmlEncode(entry.GrantedThrough)}
"); + sb.AppendLine("
"); + + // Inline JS + sb.AppendLine(""); + 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) + { + var html = BuildHtml(entries); + await File.WriteAllTextAsync(filePath, html, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct); + } + + /// Returns the CSS class for the object-type badge. + private static string ObjectTypeCss(string t) => t switch + { + "Site Collection" => "badge site-coll", + "Site" => "badge site", + "List" => "badge list", + "Folder" => "badge folder", + _ => "badge" + }; + + /// Minimal HTML encoding for text content and attribute values. + private static string HtmlEncode(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("'", "'"); + } }