feat(work-orders): allow Extra Docs PDF/DOC by category

This commit is contained in:
Arthur Bassi 2026-09-08 11:02:40 -03:00
parent a5d1c85d53
commit 47022c7761
2 changed files with 139 additions and 7 deletions

View file

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Http;
using Data.SeaHavenIndustries.Enums;
using System.IO.Compression;
namespace SeaHaven.Services.Helpers
{
@ -10,7 +12,10 @@ namespace SeaHaven.Services.Helpers
"image/jpeg",
"image/png",
"video/mp4",
"video/quicktime"
"video/quicktime",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
};
private static readonly Dictionary<string, HashSet<string>> ExtensionsByContentType =
@ -19,10 +24,16 @@ namespace SeaHaven.Services.Helpers
["image/jpeg"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg" },
["image/png"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png" },
["video/mp4"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mp4" },
["video/quicktime"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mov" }
["video/quicktime"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mov" },
["application/pdf"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".pdf" },
["application/msword"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".doc" },
["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] =
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".docx" }
};
public static bool IsAllowed(IFormFile file)
public static bool IsAllowed(
IFormFile file,
WorkOrderMediaCategory? category = WorkOrderMediaCategory.Extra)
{
if (file == null || file.Length <= 0)
return false;
@ -31,6 +42,13 @@ namespace SeaHaven.Services.Helpers
if (string.IsNullOrWhiteSpace(contentType) || !AllowedContentTypes.Contains(contentType))
return false;
var resolvedCategory = category ?? WorkOrderMediaCategory.Extra;
if (IsDocument(contentType)
&& resolvedCategory is not WorkOrderMediaCategory.Extra and not WorkOrderMediaCategory.Aveta)
{
return false;
}
var extension = Path.GetExtension(file.FileName ?? string.Empty);
if (string.IsNullOrWhiteSpace(extension)
|| !ExtensionsByContentType.TryGetValue(contentType, out var allowedExtensions)
@ -42,7 +60,7 @@ namespace SeaHaven.Services.Helpers
try
{
using var stream = file.OpenReadStream();
var headerLength = (int)Math.Min(Math.Max(file.Length, 0), 64);
var headerLength = (int)Math.Min(Math.Max(file.Length, 0), 512);
if (headerLength == 0)
return false;
@ -54,6 +72,9 @@ namespace SeaHaven.Services.Helpers
if (read < header.Length)
Array.Resize(ref header, read);
if (IsDocx(contentType))
return IsWordDocumentArchive(stream);
return MatchesSignature(contentType, header);
}
catch
@ -62,13 +83,15 @@ namespace SeaHaven.Services.Helpers
}
}
public static void EnsureAllowed(IFormFile file)
public static void EnsureAllowed(
IFormFile file,
WorkOrderMediaCategory? category = WorkOrderMediaCategory.Extra)
{
if (!IsAllowed(file))
if (!IsAllowed(file, category))
{
throw new Exceptions.WorkOrderBoardValidationException(
"UnsupportedMediaType",
"Supported media types are JPG, PNG, MP4, and MOV.");
"Supported file types are JPG, PNG, MP4, MOV, PDF, DOC, and DOCX for Extra Docs; photos accept media only.");
}
}
@ -95,9 +118,43 @@ namespace SeaHaven.Services.Helpers
return HasFtypBox(bytes);
}
if (contentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return bytes.Length >= 4 && bytes.AsSpan(0, 4).SequenceEqual("%PDF"u8);
if (contentType.Equals("application/msword", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<byte> oleSignature = stackalloc byte[]
{
0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1
};
return bytes.Length >= oleSignature.Length
&& bytes.AsSpan(0, oleSignature.Length).SequenceEqual(oleSignature);
}
return false;
}
private static bool IsDocument(string contentType)
=> contentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)
|| contentType.Equals("application/msword", StringComparison.OrdinalIgnoreCase)
|| IsDocx(contentType);
private static bool IsDocx(string contentType)
=> contentType.Equals(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
StringComparison.OrdinalIgnoreCase);
private static bool IsWordDocumentArchive(Stream stream)
{
if (!stream.CanSeek)
return false;
stream.Position = 0;
using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);
return archive.Entries.Any(entry =>
entry.FullName.StartsWith("word/", StringComparison.OrdinalIgnoreCase));
}
private static bool HasFtypBox(byte[] bytes)
{
if (bytes.Length < 12)

View file

@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Text;
using System.IO.Compression;
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.AspNetCore.Http;
@ -1741,6 +1742,19 @@ public class WorkOrderMediaFileRulesTests
ContentType = contentType
};
private static byte[] DocxBytes(bool includeWordEntry)
{
using var stream = new MemoryStream();
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
{
var entry = archive.CreateEntry(includeWordEntry ? "word/document.xml" : "not-word/content.xml");
using var writer = new StreamWriter(entry.Open());
writer.Write("<document />");
}
return stream.ToArray();
}
[Fact]
public void IsAllowed_ValidJpeg_ReturnsTrue()
{
@ -1790,4 +1804,65 @@ public class WorkOrderMediaFileRulesTests
WorkOrderMediaFileRules.EnsureAllowed(FormFile(new byte[] { 0x00 }, "a.png", "image/png")));
Assert.Equal("UnsupportedMediaType", ex.Code);
}
[Theory]
[InlineData(WorkOrderMediaCategory.Extra)]
[InlineData(WorkOrderMediaCategory.Aveta)]
public void EnsureAllowed_PdfForDocumentCategory_Succeeds(WorkOrderMediaCategory category)
{
var pdf = Encoding.ASCII.GetBytes("%PDF-1.7");
WorkOrderMediaFileRules.EnsureAllowed(
FormFile(pdf, "report.pdf", "application/pdf"),
category);
}
[Fact]
public void EnsureAllowed_DocWithOleSignatureForExtra_Succeeds()
{
var doc = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1, 0x00 };
WorkOrderMediaFileRules.EnsureAllowed(
FormFile(doc, "report.doc", "application/msword"),
WorkOrderMediaCategory.Extra);
}
[Fact]
public void EnsureAllowed_RealDocxForExtra_Succeeds()
{
WorkOrderMediaFileRules.EnsureAllowed(
FormFile(
DocxBytes(includeWordEntry: true),
"report.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
WorkOrderMediaCategory.Extra);
}
[Theory]
[InlineData(WorkOrderMediaCategory.Before)]
[InlineData(WorkOrderMediaCategory.After)]
public void EnsureAllowed_PdfForPhotoCategory_ThrowsUnsupportedMediaType(
WorkOrderMediaCategory category)
{
var ex = Assert.Throws<WorkOrderBoardValidationException>(() =>
WorkOrderMediaFileRules.EnsureAllowed(
FormFile(Encoding.ASCII.GetBytes("%PDF-1.7"), "report.pdf", "application/pdf"),
category));
Assert.Equal("UnsupportedMediaType", ex.Code);
}
[Fact]
public void EnsureAllowed_ZipWithoutWordEntry_ThrowsUnsupportedMediaType()
{
var ex = Assert.Throws<WorkOrderBoardValidationException>(() =>
WorkOrderMediaFileRules.EnsureAllowed(
FormFile(
DocxBytes(includeWordEntry: false),
"report.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
WorkOrderMediaCategory.Extra));
Assert.Equal("UnsupportedMediaType", ex.Code);
}
}