mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
feat(work-orders): allow Extra Docs PDF/DOC by category
This commit is contained in:
parent
a5d1c85d53
commit
47022c7761
2 changed files with 139 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue