mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 03:42:12 +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 Microsoft.AspNetCore.Http;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
namespace SeaHaven.Services.Helpers
|
namespace SeaHaven.Services.Helpers
|
||||||
{
|
{
|
||||||
|
|
@ -10,7 +12,10 @@ namespace SeaHaven.Services.Helpers
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/png",
|
"image/png",
|
||||||
"video/mp4",
|
"video/mp4",
|
||||||
"video/quicktime"
|
"video/quicktime",
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<string, HashSet<string>> ExtensionsByContentType =
|
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/jpeg"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg" },
|
||||||
["image/png"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png" },
|
["image/png"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png" },
|
||||||
["video/mp4"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".mp4" },
|
["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)
|
if (file == null || file.Length <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -31,6 +42,13 @@ namespace SeaHaven.Services.Helpers
|
||||||
if (string.IsNullOrWhiteSpace(contentType) || !AllowedContentTypes.Contains(contentType))
|
if (string.IsNullOrWhiteSpace(contentType) || !AllowedContentTypes.Contains(contentType))
|
||||||
return false;
|
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);
|
var extension = Path.GetExtension(file.FileName ?? string.Empty);
|
||||||
if (string.IsNullOrWhiteSpace(extension)
|
if (string.IsNullOrWhiteSpace(extension)
|
||||||
|| !ExtensionsByContentType.TryGetValue(contentType, out var allowedExtensions)
|
|| !ExtensionsByContentType.TryGetValue(contentType, out var allowedExtensions)
|
||||||
|
|
@ -42,7 +60,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = file.OpenReadStream();
|
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)
|
if (headerLength == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -54,6 +72,9 @@ namespace SeaHaven.Services.Helpers
|
||||||
if (read < header.Length)
|
if (read < header.Length)
|
||||||
Array.Resize(ref header, read);
|
Array.Resize(ref header, read);
|
||||||
|
|
||||||
|
if (IsDocx(contentType))
|
||||||
|
return IsWordDocumentArchive(stream);
|
||||||
|
|
||||||
return MatchesSignature(contentType, header);
|
return MatchesSignature(contentType, header);
|
||||||
}
|
}
|
||||||
catch
|
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(
|
throw new Exceptions.WorkOrderBoardValidationException(
|
||||||
"UnsupportedMediaType",
|
"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);
|
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;
|
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)
|
private static bool HasFtypBox(byte[] bytes)
|
||||||
{
|
{
|
||||||
if (bytes.Length < 12)
|
if (bytes.Length < 12)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.IO.Compression;
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -1741,6 +1742,19 @@ public class WorkOrderMediaFileRulesTests
|
||||||
ContentType = contentType
|
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]
|
[Fact]
|
||||||
public void IsAllowed_ValidJpeg_ReturnsTrue()
|
public void IsAllowed_ValidJpeg_ReturnsTrue()
|
||||||
{
|
{
|
||||||
|
|
@ -1790,4 +1804,65 @@ public class WorkOrderMediaFileRulesTests
|
||||||
WorkOrderMediaFileRules.EnsureAllowed(FormFile(new byte[] { 0x00 }, "a.png", "image/png")));
|
WorkOrderMediaFileRules.EnsureAllowed(FormFile(new byte[] { 0x00 }, "a.png", "image/png")));
|
||||||
Assert.Equal("UnsupportedMediaType", ex.Code);
|
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