using Microsoft.AspNetCore.Http; namespace SeaHaven.Services.Helpers { /// SH-337 file type allowlist for work-order completion documents. public static class WorkOrderCompletionDocFileRules { private const string PdfContentType = "application/pdf"; private static readonly HashSet AllowedExtensions = new(StringComparer.OrdinalIgnoreCase) { ".pdf" }; // A browser sets the multipart part's Content-Type from File.type, which the OS // sometimes leaves empty for a PDF. The completion-doc dialog already accepts // that case on the client (a .pdf name with no type passes), so rejecting it // here would fail uploads that work today. The %PDF- signature is the real gate. private static readonly HashSet UndeterminedContentTypes = new(StringComparer.OrdinalIgnoreCase) { "application/octet-stream" }; public static bool IsAllowed(IFormFile file) { if (file == null || file.Length <= 0) return false; var declaredType = (file.ContentType ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(declaredType) && !declaredType.Equals(PdfContentType, StringComparison.OrdinalIgnoreCase) && !UndeterminedContentTypes.Contains(declaredType)) { return false; } var extension = Path.GetExtension(file.FileName ?? string.Empty); if (string.IsNullOrWhiteSpace(extension) || !AllowedExtensions.Contains(extension)) return false; try { using var stream = file.OpenReadStream(); var headerLength = (int)Math.Min(Math.Max(file.Length, 0), 64); if (headerLength == 0) return false; var header = new byte[headerLength]; var read = stream.Read(header, 0, header.Length); if (read <= 0) return false; if (read < header.Length) Array.Resize(ref header, read); return WorkOrderMediaFileRules.MatchesSignature(PdfContentType, header); } catch { return false; } } public static void EnsureAllowed(IFormFile file) { if (!IsAllowed(file)) { throw new Exceptions.WorkOrderBoardValidationException( "UnsupportedMediaType", "The completion document must be a PDF file."); } } } }