using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using AyaNova.Models; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using AyaNova.Util; using System.IO; namespace AyaNova.Api.Controllers { /// /// Logo controller /// [ApiController] [Asp.Versioning.ApiVersion("8.0")] [Route("api/v{version:apiVersion}/logo")] [Produces("application/json")] [Authorize] public class LogoController : ControllerBase { private readonly AyContext ct; private readonly ILogger log; private readonly ApiServerState serverState; // private const int MAXIMUM_LOGO_SIZE = 512000;//We really don't want it too big or it will slow the fuck down for everything /// /// ctor /// /// /// /// public LogoController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) { ct = dbcontext; log = logger; serverState = apiServerState; } /// /// Get Logo /// /// One of "small", "medium", "large" /// A single Logo and it's values [AllowAnonymous] [HttpGet("{size}")] public async Task DownloadLogo([FromRoute] string size) { //allowing this because it messes up the login form needlessly // if (serverState.IsClosed) // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); if (string.IsNullOrWhiteSpace(size)) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "size", "Size is required and must be one of 'small', 'medium' or 'large'")); size = size.ToLowerInvariant(); if (size != "small" && size != "medium" && size != "large") return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "size", "Size parameter must be one of 'small', 'medium' or 'large'")); var logo = await ct.Logo.SingleOrDefaultAsync(); if (logo == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); switch (size) { case "small": if (logo.Small == null) return NotFound(); return LogoFile(logo.Small); case "medium": if (logo.Medium == null) return NotFound(); return LogoFile(logo.Medium); case "large": if (logo.Large == null) return NotFound(); return LogoFile(logo.Large); } return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } //case 4727 (F23): the type comes from the bytes, never from what was sent or stored, and the response may not run //script: an SVG opened directly by its URL is shown, its script is not run (an tag is not affected) private FileStreamResult LogoFile(byte[] bytes) { Response.Headers["X-Content-Type-Options"] = "nosniff"; Response.Headers["Content-Security-Policy"] = "default-src 'none'; style-src 'unsafe-inline'; sandbox"; return new FileStreamResult(new MemoryStream(bytes), ImageTypeOrNull(bytes) ?? "application/octet-stream"); } //case 4727 (F23): PNG, JPEG, GIF, WebP or SVG, recognised by their content; null for anything else internal static string ImageTypeOrNull(byte[] b) { if (b == null || b.Length < 12) return null; if (b[0] == 0x89 && b[1] == 0x50 && b[2] == 0x4E && b[3] == 0x47 && b[4] == 0x0D && b[5] == 0x0A && b[6] == 0x1A && b[7] == 0x0A) return "image/png"; if (b[0] == 0xFF && b[1] == 0xD8 && b[2] == 0xFF) return "image/jpeg"; if (b[0] == 'G' && b[1] == 'I' && b[2] == 'F' && b[3] == '8' && (b[4] == '7' || b[4] == '9') && b[5] == 'a') return "image/gif"; if (b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F' && b[8] == 'W' && b[9] == 'E' && b[10] == 'B' && b[11] == 'P') return "image/webp"; var head = System.Text.Encoding.UTF8.GetString(b, 0, System.Math.Min(b.Length, 4096)).TrimStart((char)0xFEFF, ' ', '\t', '\r', '\n'); if (head.StartsWith("<") && head.Contains(" /// Upload Logo /// Max 500 KiB total (512000 bytes) /// Must have full rights to Global object /// /// One of "small", "medium", "large" /// Accepted [Authorize] [HttpPost("{size}")] //[DisableFormValueModelBinding] [RequestSizeLimit(ServerBootConfig.MAX_LOGO_UPLOAD_BYTES)] public async Task UploadAsync([FromRoute] string size) { //https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1#upload-small-files-with-buffered-model-binding-to-a-database if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); //case 4727 (F23): change rights (was read rights, which BizAdminRestricted has) if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Global)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (string.IsNullOrWhiteSpace(size)) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "size", "Size is required and must be one of 'small', 'medium' or 'large'")); size = size.ToLowerInvariant(); if (size != "small" && size != "medium" && size != "large") return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "size", "Size parameter must be one of 'small', 'medium' or 'large'")); //get the one and only logo object var logo = await ct.Logo.FirstOrDefaultAsync(); if (logo == null) { logo = new Logo(); ct.Logo.Add(logo); await ct.SaveChangesAsync(); } var file = Request.Form.Files[0]; //var file=files[0]; using (var memoryStream = new MemoryStream()) { await file.CopyToAsync(memoryStream); if (memoryStream.Length > ServerBootConfig.MAX_LOGO_UPLOAD_BYTES) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, null, $"Logo files must be smaller than {ServerBootConfig.MAX_LOGO_UPLOAD_BYTES} maximum")); var imageType = ImageTypeOrNull(memoryStream.ToArray()); if (imageType == null) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, "Logo files must be PNG, JPEG, GIF, WebP or SVG images")); switch (size) { case "small": logo.Small = memoryStream.ToArray(); logo.SmallType = imageType; break; case "medium": logo.Medium = memoryStream.ToArray(); logo.MediumType = imageType; break; case "large": logo.Large = memoryStream.ToArray(); logo.LargeType = imageType; break; } await ct.SaveChangesAsync(); } return Accepted(); } /// /// Delete logo /// /// /// NoContent [Authorize] [HttpDelete("{size}")] public async Task DeleteLogo([FromRoute] string size) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); //case 4727 (F23): change rights (was read rights, which BizAdminRestricted has) if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Global)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (string.IsNullOrWhiteSpace(size)) return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, "size", "Size is required and must be one of 'small', 'medium' or 'large'")); size = size.ToLowerInvariant(); if (size != "small" && size != "medium" && size != "large") return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "size", "Size parameter must be one of 'small', 'medium' or 'large'")); //get the one and only logo object var logo = await ct.Logo.FirstOrDefaultAsync(); if (logo != null) { switch (size) { case "small": logo.Small = null; logo.SmallType = string.Empty; break; case "medium": logo.Medium = null; logo.MediumType = string.Empty; break; case "large": logo.Large = null; logo.LargeType = string.Empty; break; } await ct.SaveChangesAsync(); } return NoContent(); } } }