From 35ce2034f7d3d6d30073ee5a30f93bd6363a2506 Mon Sep 17 00:00:00 2001 From: schnulller Date: Thu, 2 Jun 2022 19:26:43 +0200 Subject: [PATCH] Added Classes for detecting Root Elements of Hamann-Collections --- HaWeb/Controllers/UploadController.cs | 80 +++++++++++++-------- HaWeb/FileHelpers/XDocumentFileHelpers.cs | 38 ++++++++++ HaWeb/FileHelpers/XMLFileHelpers.cs | 12 ++-- HaWeb/HaWeb.csproj | 2 +- HaWeb/Models/UploadViewModel.cs | 5 ++ HaWeb/Program.cs | 19 +++-- HaWeb/Settings/GeneralSettings.cs | 1 + HaWeb/Settings/XMLRoots/CommentRoot.cs | 36 ++++++++++ HaWeb/Settings/XMLRoots/DescriptionsRoot.cs | 33 +++++++++ HaWeb/Settings/XMLRoots/DocumentRoot.cs | 33 +++++++++ HaWeb/Settings/XMLRoots/EditsRoot.cs | 33 +++++++++ HaWeb/Settings/XMLRoots/MarginalsRoot.cs | 33 +++++++++ HaWeb/Settings/XMLRoots/ReferencesRoot.cs | 31 ++++++++ HaWeb/Settings/XMLRoots/TraditionsRoot.cs | 33 +++++++++ HaWeb/Views/Admin/Upload/Index.cshtml | 6 ++ HaWeb/XMLParser/IXMLRoot.cs | 47 ++++++++++++ HaWeb/XMLParser/IXMLService.cs | 8 +++ HaWeb/XMLParser/XMLRootDocument.cs | 41 +++++++++++ HaWeb/XMLParser/XMLService.cs | 72 +++++++++++++++++++ HaWeb/appsettings.json | 3 +- 20 files changed, 526 insertions(+), 40 deletions(-) create mode 100644 HaWeb/FileHelpers/XDocumentFileHelpers.cs create mode 100644 HaWeb/Models/UploadViewModel.cs create mode 100644 HaWeb/Settings/XMLRoots/CommentRoot.cs create mode 100644 HaWeb/Settings/XMLRoots/DescriptionsRoot.cs create mode 100644 HaWeb/Settings/XMLRoots/DocumentRoot.cs create mode 100644 HaWeb/Settings/XMLRoots/EditsRoot.cs create mode 100644 HaWeb/Settings/XMLRoots/MarginalsRoot.cs create mode 100644 HaWeb/Settings/XMLRoots/ReferencesRoot.cs create mode 100644 HaWeb/Settings/XMLRoots/TraditionsRoot.cs create mode 100644 HaWeb/XMLParser/IXMLRoot.cs create mode 100644 HaWeb/XMLParser/IXMLService.cs create mode 100644 HaWeb/XMLParser/XMLRootDocument.cs create mode 100644 HaWeb/XMLParser/XMLService.cs diff --git a/HaWeb/Controllers/UploadController.cs b/HaWeb/Controllers/UploadController.cs index 1a4efb6..ef7d529 100644 --- a/HaWeb/Controllers/UploadController.cs +++ b/HaWeb/Controllers/UploadController.cs @@ -6,6 +6,7 @@ using Microsoft.FeatureManagement.Mvc; using System.IO; using System.Net; using System.Text; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -14,6 +15,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Net.Http.Headers; using HaWeb.Filters; using HaWeb.FileHelpers; +using HaWeb.XMLParser; +using HaWeb.Models; +using System.Xml.Linq; public class UploadController : Controller { @@ -22,18 +26,24 @@ public class UploadController : Controller private IReaderService _readerService; private readonly long _fileSizeLimit; private readonly string _targetFilePath; + private readonly IXMLService _xmlService; // Options private static readonly string[] _permittedExtensions = { ".xml" }; private static readonly FormOptions _defaultFormOptions = new FormOptions(); - public UploadController(ILibrary lib, IReaderService readerService, IConfiguration config) + public UploadController(ILibrary lib, IReaderService readerService, IXMLService xmlService, IConfiguration config) { _lib = lib; _readerService = readerService; + _xmlService = xmlService; _fileSizeLimit = config.GetValue("FileSizeLimit"); - _targetFilePath = config.GetValue("StoredFilesPath"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + _targetFilePath = config.GetValue("StoredFilePathWindows"); + } else { + _targetFilePath = config.GetValue("StoredFilePathLinux"); + } } [HttpGet] @@ -42,71 +52,85 @@ public class UploadController : Controller [GenerateAntiforgeryTokenCookie] public IActionResult Index() { - return View("../Admin/Upload/Index"); + var model = new UploadViewModel(); + model.AvailableRoots = _xmlService.GetRoots().Select(x => (x.Type, "")).ToList(); + return View("../Admin/Upload/Index", model); } + +//// UPLOAD //// [HttpPost] [Route("Admin/Upload")] [DisableFormValueModelBinding] [ValidateAntiForgeryToken] public async Task Post() { - -//// 1. Stage: Check Request and File on a byte-level - // Checks the COntent-Type Field (must be multipart + Boundary) +//// 1. Stage: Check Request format and request spec + // Checks the Content-Type Field (must be multipart + Boundary) if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) { - ModelState.AddModelError("File", $"Wrong / No Content Type on the Request"); + ModelState.AddModelError("Error", $"Wrong / No Content Type on the Request"); return BadRequest(ModelState); } // Divides the multipart document into it's sections and sets up a reader var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, HttpContext.Request.Body); - var section = await reader.ReadNextSectionAsync(); + MultipartSection? section = null; + try { + section = await reader.ReadNextSectionAsync(); + } + catch (Exception ex) { + ModelState.AddModelError("Error", "The Request is bad: " + ex.Message); + } while (section != null) { // Multipart document content disposition header read for a section: // Starts with boundary, contains field name, content-dispo, filename, content-type var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); - if (hasContentDispositionHeader && contentDisposition != null) { // Checks if it is a section with content-disposition, name, filename if (!MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) { - ModelState.AddModelError("File", $"Wrong Content-Dispostion Headers in Multipart Document"); + ModelState.AddModelError("Error", $"Wrong Content-Dispostion Headers in Multipart Document"); return BadRequest(ModelState); } - // Sanity checks on the file on a byte level, extension checking, is it empty etc. +//// 2. Stage: Check File. Sanity checks on the file on a byte level, extension checking, is it empty etc. var streamedFileContent = await XMLFileHelpers.ProcessStreamedFile( section, contentDisposition, ModelState, _permittedExtensions, _fileSizeLimit); - - if (!ModelState.IsValid) + if (!ModelState.IsValid || streamedFileContent == null) return BadRequest(ModelState); -//// 2. Stage: Valid XML checking +//// 3. Stage: Valid XML checking using a simple XDocument.Load() + var xdocument = await XDocumentFileHelper.ProcessStreamedFile(streamedFileContent, ModelState); + if (!ModelState.IsValid || xdocument == null) + return UnprocessableEntity(ModelState); //// 3. Stage: Is it a Hamann-Document? What kind? + var docs = _xmlService.ProbeHamannFile(xdocument, ModelState); + if (!ModelState.IsValid || docs == null || !docs.Any()) + return UnprocessableEntity(ModelState); + +//// 5. Stage: Saving the File(s) + foreach (var doc in docs) { + using (var targetStream = System.IO.File.Create(Path.Combine(_targetFilePath, doc.CreateFilename()))) + doc.Save(targetStream); + } -//// 4. Stage: Get Filename for the stageing area - -//// 5. Stage: Saving the File - // // Encode Filename for display - // var trustedFileNameForDisplay = WebUtility.HtmlEncode(contentDisposition.FileName.Value); - - - // // TODO: generatre storage filename - // var trustedFileNameForFileStorage = Path.GetRandomFileName(); - // using (var targetStream = System.IO.File.Create(Path.Combine(_targetFilePath, trustedFileNameForFileStorage))) - // await targetStream.WriteAsync(streamedFileContent); + return Created(nameof(UploadController), docs); } - // Drain any remaining section body that hasn't been consumed and - // read the headers for the next section. - section = await reader.ReadNextSectionAsync(); + try + { + section = await reader.ReadNextSectionAsync(); + } + catch (Exception ex) + { + ModelState.AddModelError("Error", "The Request is bad: " + ex.Message); + } } //// Success! Return Last Created File View diff --git a/HaWeb/FileHelpers/XDocumentFileHelpers.cs b/HaWeb/FileHelpers/XDocumentFileHelpers.cs new file mode 100644 index 0000000..e747834 --- /dev/null +++ b/HaWeb/FileHelpers/XDocumentFileHelpers.cs @@ -0,0 +1,38 @@ +namespace HaWeb.FileHelpers; +using System.Xml.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.Text; +using System.Xml; + +public static class XDocumentFileHelper { + + private readonly static XmlReaderSettings _Settings = new XmlReaderSettings() + { + CloseInput = true, + CheckCharacters = false, + ConformanceLevel = ConformanceLevel.Fragment, + IgnoreComments = true, + IgnoreProcessingInstructions = true, + IgnoreWhitespace = false + }; + + public static async Task ProcessStreamedFile(byte[] bytes, ModelStateDictionary modelState) { + try + { + using (var stream = new MemoryStream(bytes)) + { + using (var xmlreader = XmlReader.Create(stream, _Settings)) + { + return XDocument.Load(xmlreader, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); + + } + } + } + catch (Exception ex) + { + modelState.AddModelError("Error", $"Kein gültiges XML-Dokument geladen. Error: {ex.Message}"); + } + + return null; + } +} \ No newline at end of file diff --git a/HaWeb/FileHelpers/XMLFileHelpers.cs b/HaWeb/FileHelpers/XMLFileHelpers.cs index cdd914b..07b5341 100644 --- a/HaWeb/FileHelpers/XMLFileHelpers.cs +++ b/HaWeb/FileHelpers/XMLFileHelpers.cs @@ -140,7 +140,7 @@ public static class XMLFileHelpers // return Array.Empty(); // } - public static async Task ProcessStreamedFile( + public static async Task ProcessStreamedFile( MultipartSection section, ContentDispositionHeaderValue contentDisposition, ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) { @@ -152,16 +152,16 @@ public static class XMLFileHelpers // Check if the file is empty or exceeds the size limit. if (memoryStream.Length == 0) - modelState.AddModelError("File", "The file is empty."); + modelState.AddModelError("Error", "The file is empty."); else if (memoryStream.Length > sizeLimit) { var megabyteSizeLimit = sizeLimit / 1048576; - modelState.AddModelError("File", $"The file exceeds {megabyteSizeLimit:N1} MB."); + modelState.AddModelError("Error", $"The file exceeds {megabyteSizeLimit:N1} MB."); } // Check file extension and first bytes else if (!IsValidFileExtensionAndSignature(contentDisposition.FileName.Value, memoryStream, permittedExtensions)) - modelState.AddModelError("File", "The file must be of the following specs:
" + + modelState.AddModelError("Error", "The file must be of the following specs:
" + "1. The file must hava a .xml File-Extension
" + "2. To make sure the file isn't executable the file must start with: or "); @@ -171,10 +171,10 @@ public static class XMLFileHelpers } catch (Exception ex) { - modelState.AddModelError("File", $"The upload failed. Error: {ex.HResult}"); + modelState.AddModelError("Error", $"The upload failed. Error: {ex.Message}"); } - return Array.Empty(); + return null; } private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, string[] permittedExtensions) diff --git a/HaWeb/HaWeb.csproj b/HaWeb/HaWeb.csproj index fd3f5d4..ffbf3e8 100644 --- a/HaWeb/HaWeb.csproj +++ b/HaWeb/HaWeb.csproj @@ -2,6 +2,7 @@ net6.0 + True enable enable @@ -24,7 +25,6 @@ - diff --git a/HaWeb/Models/UploadViewModel.cs b/HaWeb/Models/UploadViewModel.cs new file mode 100644 index 0000000..ce57f3d --- /dev/null +++ b/HaWeb/Models/UploadViewModel.cs @@ -0,0 +1,5 @@ +namespace HaWeb.Models; + +public class UploadViewModel { + public List<(string, string)>? AvailableRoots { get; set; } +} \ No newline at end of file diff --git a/HaWeb/Program.cs b/HaWeb/Program.cs index a611319..9abd694 100644 --- a/HaWeb/Program.cs +++ b/HaWeb/Program.cs @@ -1,18 +1,29 @@ using HaXMLReader; using HaXMLReader.Interfaces; using HaDocument.Interfaces; +using HaWeb.XMLParser; using Microsoft.FeatureManagement; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddHttpContextAccessor(); -builder.Services.AddSingleton(x => HaDocument.Document.Create(new Options())); -builder.Services.AddTransient(); -builder.Services.AddFeatureManagement(); -// builder.Services.AddWebOptimizer(); +// // To list physical files from a path provided by configuration: +// var physicalProvider = new PhysicalFileProvider(Configuration.GetValue("StoredFilesPath")); +// // To list physical files in the temporary files folder, use: +// //var physicalProvider = new PhysicalFileProvider(Path.GetTempPath()); +// services.AddSingleton(physicalProvider); + +builder.Services.AddSingleton(HaDocument.Document.Create(new Options())); +builder.Services.AddTransient(); +builder.Services.AddSingleton(); +builder.Services.AddFeatureManagement(); var app = builder.Build(); diff --git a/HaWeb/Settings/GeneralSettings.cs b/HaWeb/Settings/GeneralSettings.cs index 0ecda11..c177c25 100644 --- a/HaWeb/Settings/GeneralSettings.cs +++ b/HaWeb/Settings/GeneralSettings.cs @@ -1,4 +1,5 @@ namespace HaWeb.Settings; +using HaWeb.Settings.XMLRoots; public static class General { // Classes generated by parsing the XML: diff --git a/HaWeb/Settings/XMLRoots/CommentRoot.cs b/HaWeb/Settings/XMLRoots/CommentRoot.cs new file mode 100644 index 0000000..c434cfc --- /dev/null +++ b/HaWeb/Settings/XMLRoots/CommentRoot.cs @@ -0,0 +1,36 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class CommentRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Register"; + public string Container { get; } = "kommcat"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "kommentar") return true; + else return false; + }; + + public Func GetKey { get; } = (elem) => { + var index = elem.Attribute("id"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + else return null; + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + var kat = element.Attribute("value"); + if (kat != null && !String.IsNullOrWhiteSpace(kat.Value)) + return (null, kat.Value); + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs b/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs new file mode 100644 index 0000000..64bfa90 --- /dev/null +++ b/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs @@ -0,0 +1,33 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class DescriptionsRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Metadaten"; + public string Container { get; } = "descriptions"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "letterDesc") return true; + return false; + }; + + public Func GetKey { get; } = (elem) => { + var index = elem.Attribute("ref"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + else return null; + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/DocumentRoot.cs b/HaWeb/Settings/XMLRoots/DocumentRoot.cs new file mode 100644 index 0000000..f721188 --- /dev/null +++ b/HaWeb/Settings/XMLRoots/DocumentRoot.cs @@ -0,0 +1,33 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class DocumentRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Brieftext"; + public string Container { get; } = "document"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "letterText") return true; + else return false; + }; + + public Func GetKey { get; } = (elem) => { + var index = elem.Attribute("index"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + else return null; + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/EditsRoot.cs b/HaWeb/Settings/XMLRoots/EditsRoot.cs new file mode 100644 index 0000000..99e2971 --- /dev/null +++ b/HaWeb/Settings/XMLRoots/EditsRoot.cs @@ -0,0 +1,33 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class EditsRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Texteingriffe"; + public string Container { get; } = "edits"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "editreason") return true; + else return false; + }; + + public Func GetKey { get; } = (elem) => { + var index = elem.Attribute("index"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + else return null; + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/MarginalsRoot.cs b/HaWeb/Settings/XMLRoots/MarginalsRoot.cs new file mode 100644 index 0000000..8336688 --- /dev/null +++ b/HaWeb/Settings/XMLRoots/MarginalsRoot.cs @@ -0,0 +1,33 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class MarginalsRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Stellenkommentar"; + public string Container { get; } = "marginalien"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "marginal") return true; + else return false; + }; + + public Func GetKey { get; } = (elem) => { + var index = elem.Attribute("index"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + else return null; + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/ReferencesRoot.cs b/HaWeb/Settings/XMLRoots/ReferencesRoot.cs new file mode 100644 index 0000000..0b31d26 --- /dev/null +++ b/HaWeb/Settings/XMLRoots/ReferencesRoot.cs @@ -0,0 +1,31 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class ReferencesRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Personen / Orte"; + public string Container { get; } = "definitions"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "personDefs" || elem.Name == "structureDefs" || elem.Name == "handDefs" || elem.Name == "locationDefs") + return true; + return false; + }; + + public Func GetKey { get; } = (elem) => { + return elem.Name.ToString(); + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/TraditionsRoot.cs b/HaWeb/Settings/XMLRoots/TraditionsRoot.cs new file mode 100644 index 0000000..7b577a0 --- /dev/null +++ b/HaWeb/Settings/XMLRoots/TraditionsRoot.cs @@ -0,0 +1,33 @@ +namespace HaWeb.Settings.XMLRoots; +using System.Xml.Linq; +using HaWeb.XMLParser; + +public class TraditionsRoot : HaWeb.XMLParser.IXMLRoot { + public string Type { get; } = "Überlieferung"; + public string Container { get; } = "traditions"; + + public Predicate IsCollectedObject { get; } = (elem) => { + if (elem.Name == "letterTradition") return true; + else return false; + }; + + public Func GetKey { get; } = (elem) => { + var index = elem.Attribute("ref"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + else return null; + }; + + public List<(string, string)>? GenerateFields(XMLRootDocument document) { + return null; + } + + public (string?, string) GenerateIdentificationString(XElement element) { + return (null, Container); + } + + public bool Replaces(XMLRootDocument doc1, XMLRootDocument doc2) { + return true; + } + +} \ No newline at end of file diff --git a/HaWeb/Views/Admin/Upload/Index.cshtml b/HaWeb/Views/Admin/Upload/Index.cshtml index 854e771..c72d97b 100644 --- a/HaWeb/Views/Admin/Upload/Index.cshtml +++ b/HaWeb/Views/Admin/Upload/Index.cshtml @@ -1,3 +1,4 @@ +@model UploadViewModel; Hello from Upload Index!
+@foreach (var item in Model.AvailableRoots.OrderBy(x => x.Item1)) +{ +
@item.Item1
+} + @section Scripts {