Files
hamann-ausgabe-core/HaWeb/Controllers/APIController.cs

350 lines
14 KiB
C#

namespace HaWeb.Controllers;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Net.Http.Headers;
using HaWeb.Filters;
using HaWeb.FileHelpers;
using HaWeb.XMLParser;
using HaWeb.Models;
using System.Text.Json.Serialization;
using System.Text.Json;
using HaDocument.Interfaces;
using HaXMLReader.Interfaces;
using Microsoft.FeatureManagement.Mvc;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Http.Features;
using System.Text;
// Controlling all the API-Endpoints
public class APIController : Controller {
// DI
private IHaDocumentWrappper _lib;
private IReaderService _readerService;
private readonly long _fileSizeLimit;
private readonly string _targetFilePath;
private readonly IXMLService _xmlService;
private readonly IXMLProvider _xmlProvider;
// Options
private static readonly string[] _permittedExtensions = { ".xml" };
private static readonly FormOptions _defaultFormOptions = new FormOptions();
public APIController(IHaDocumentWrappper lib, IReaderService readerService, IXMLService xmlService, IXMLProvider xmlProvider, IConfiguration config) {
_lib = lib;
_xmlProvider = xmlProvider;
_readerService = readerService;
_xmlService = xmlService;
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
_targetFilePath = config.GetValue<string>("StoredFilePathWindows");
} else {
_targetFilePath = config.GetValue<string>("StoredFilePathLinux");
}
}
[HttpGet]
[Route("API/Syntaxcheck/{id}")]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
[FeatureGate(Features.UploadService, Features.AdminService)]
public IActionResult SyntaxCheck(string id) {
return Ok();
}
//// UPLOAD ////
[HttpPost]
[Route("API/Upload")]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
[FeatureGate(Features.UploadService, Features.AdminService)]
public async Task<IActionResult> Upload() {
List<XMLRootDocument>? docs = null;
//// 1. Stage: Check Request format and request spec
// Checks the Content-Type Field (must be multipart + Boundary)
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) {
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);
MultipartSection? section = null;
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
return BadRequest(ModelState);
}
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 (contentDisposition != null && contentDisposition.Name == "__RequestVerificationToken") {
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
}
continue;
}
if (hasContentDispositionHeader && contentDisposition != null) {
// Checks if it is a section with content-disposition, name, filename
if (!MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) {
ModelState.AddModelError("Error", $"Wrong Content-Dispostion Headers in Multipart Document");
return BadRequest(ModelState);
}
//// 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 || streamedFileContent == null)
return BadRequest(ModelState);
//// 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);
//// 4. Stage: Is it a Hamann-Document? What kind?
var retdocs = _xmlService.ProbeFile(xdocument, ModelState);
if (!ModelState.IsValid || retdocs == null || !retdocs.Any())
return UnprocessableEntity(ModelState);
//// 5. Stage: Saving the File(s)
foreach (var doc in retdocs) {
// Physical saving
await _xmlProvider.Save(doc, _targetFilePath, ModelState);
// Setting the new docuemnt as used
_xmlService.Use(doc);
// Unsetting all old docuemnts as ununsed
_xmlService.AutoUse(doc.Prefix);
if (!ModelState.IsValid) return StatusCode(500, ModelState);
if (docs == null) docs = new List<XMLRootDocument>();
docs.Add(doc);
}
xdocument = null;
retdocs = null;
streamedFileContent = null;
}
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
return BadRequest(ModelState);
}
}
// 6. Stage: Success! Returning Ok, and redirecting
JsonSerializerOptions options = new() {
ReferenceHandler = ReferenceHandler.Preserve,
Converters = {
new IdentificationStringJSONConverter()
}
};
string json = JsonSerializer.Serialize(docs);
return Created(nameof(UploadController), json);
}
//// PUBLISH ////
[HttpPost]
[Route("API/LocalPublish")]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
[FeatureGate(Features.LocalPublishService, Features.AdminService, Features.UploadService)]
public async Task<IActionResult> LocalPublish() {
var element = _xmlService.MergeUsedDocuments(ModelState);
if (!ModelState.IsValid || element == null)
return BadRequest(ModelState);
var savedfile = await _xmlProvider.SaveHamannFile(element, _targetFilePath, ModelState);
if (!ModelState.IsValid || savedfile == null) {
if (savedfile != null)
_xmlProvider.DeleteHamannFile(savedfile.Name);
return BadRequest(ModelState);
}
_ = _lib.SetLibrary(savedfile.PhysicalPath, ModelState);
if (!ModelState.IsValid) {
_xmlProvider.DeleteHamannFile(savedfile.Name);
return BadRequest(ModelState);
}
_xmlProvider.SetInProduction(savedfile);
_xmlService.SetInProduction();
return Created("/", _xmlProvider.GetHamannFiles());
}
[HttpPost]
[Route("API/SetUsed/{id}")]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
[FeatureGate(Features.UploadService, Features.AdminService)]
public async Task<IActionResult> SetUsed(string id) {
var f = _xmlProvider.GetFiles(id);
if (f == null) {
ModelState.AddModelError("Error", "Wrong Endpoint");
return BadRequest(ModelState);
}
var files = f.GetFileList();
if (files == null) {
ModelState.AddModelError("Error", "Wrong Endpoint");
return BadRequest(ModelState);
}
List<XMLRootDocument>? newUsed = null;
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) {
ModelState.AddModelError("Error", $"Wrong / No Content Type on the Request");
return BadRequest(ModelState);
}
// Same as above, check Upload()
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
MultipartSection? section = null;
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
return BadRequest(ModelState);
}
while (section != null) {
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
if (contentDisposition != null && contentDisposition.Name == "__RequestVerificationToken") {
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
}
continue;
}
var filename = "";
if (hasContentDispositionHeader && contentDisposition != null) {
if (!MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition)) {
ModelState.AddModelError("Error", $"Wrong Content-Dispostion Headers in Multipart Document");
return BadRequest(ModelState);
}
filename = XMLFileHelpers.StreamToString(section.Body, ModelState);
if (!ModelState.IsValid) return BadRequest(ModelState);
var isFile = files.Where(x => x.FileName == filename);
if (isFile == null || !isFile.Any()) {
ModelState.AddModelError("Error", "Tried to add a file that does not exist.");
return BadRequest(ModelState);
}
if (newUsed == null) newUsed = new List<XMLRootDocument>();
newUsed.Add(isFile.First());
}
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
return BadRequest(ModelState);
}
}
if (newUsed != null && newUsed.Any()) {
_xmlService.UnUse(id);
newUsed.ForEach(x => _xmlService.Use(x));
}
return Created("/", newUsed);
}
[HttpPost]
[Route("API/SetUsedHamann")]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
[FeatureGate(Features.UploadService, Features.AdminService)]
public async Task<IActionResult> SetUsedHamann() {
var hF = _xmlProvider.GetHamannFiles();
if (hF == null) {
ModelState.AddModelError("Error", "There are no Hamman.xml files available.");
return BadRequest(ModelState);
}
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) {
ModelState.AddModelError("Error", $"Wrong / No Content Type on the Request");
return BadRequest(ModelState);
}
// Same as above, check Upload()
string? filename = null;
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
MultipartSection? section = null;
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
return BadRequest(ModelState);
}
while (section != null) {
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
if (contentDisposition != null && contentDisposition.Name == "__RequestVerificationToken") {
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
}
continue;
}
if (hasContentDispositionHeader && contentDisposition != null) {
if (!MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition)) {
ModelState.AddModelError("Error", $"Wrong Content-Dispostion Headers in Multipart Document");
return BadRequest(ModelState);
}
filename = XMLFileHelpers.StreamToString(section.Body, ModelState);
if (!ModelState.IsValid) return BadRequest(ModelState);
}
try {
section = await reader.ReadNextSectionAsync();
} catch (Exception ex) {
ModelState.AddModelError("Error", "The Request is bad: " + ex.Message);
return BadRequest(ModelState);
}
}
if (filename == null) {
ModelState.AddModelError("Error", "No filename given");
return BadRequest(ModelState);
}
var newFile = hF.Where(x => x.Name == filename);
if (newFile == null || !newFile.Any()) {
ModelState.AddModelError("Error", "Trying to set a unavailable file.");
return BadRequest(ModelState);
}
_ = _lib.SetLibrary(newFile.First().PhysicalPath, ModelState);
if (!ModelState.IsValid) return BadRequest(ModelState);
_xmlProvider.SetInProduction(newFile.First());
_xmlService.UnUseProduction();
return Created("/", newFile.First());
}
}