From dc88b5ccf193933a8b0eef9913b35d53edaef1bb Mon Sep 17 00:00:00 2001 From: C-xC-c Date: Sun, 15 Dec 2019 21:15:49 +0000 Subject: [PATCH] Rewrite the upload page. --- BantFlags/BantFlags.csproj | 1 + BantFlags/Data/Database/DatabaseService.cs | 46 ++-- BantFlags/Data/Staging.cs | 148 ++++++++--- BantFlags/Pages/Index.cshtml | 4 +- BantFlags/Pages/Upload.cshtml | 117 ++++---- BantFlags/Pages/Upload.cshtml.cs | 293 +++++++-------------- BantFlags/Startup.cs | 1 - BantFlags/wwwroot/upload.css | 4 + 8 files changed, 290 insertions(+), 324 deletions(-) diff --git a/BantFlags/BantFlags.csproj b/BantFlags/BantFlags.csproj index f6ba716..456cd21 100644 --- a/BantFlags/BantFlags.csproj +++ b/BantFlags/BantFlags.csproj @@ -15,6 +15,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/BantFlags/Data/Database/DatabaseService.cs b/BantFlags/Data/Database/DatabaseService.cs index dc4eaee..31e65a2 100644 --- a/BantFlags/Data/Database/DatabaseService.cs +++ b/BantFlags/Data/Database/DatabaseService.cs @@ -32,27 +32,6 @@ namespace BantFlags.Data.Database KnownFlags = flags.ToHashSet(); } - public async Task DeleteFlagsAsync(List flags) - { - using var rentedConnection = await ConnectionPool.RentConnectionAsync(); - using var query = rentedConnection.Object.UseStoredProcedure("delete_flag"); - - flags.ForEach(async f => - await query.SetParam("@flag", f.Name) - .ExecuteNonQueryAsync(reuse: true)); - } - - public async Task RenameFlagsAsync(List flags) - { - using var rentedConnection = await ConnectionPool.RentConnectionAsync(); - using var query = rentedConnection.Object.UseStoredProcedure("rename_flag"); - - flags.ForEach(async flag => - await query.SetParam("@old", flag.Name) - .SetParam("@new", flag.NewName) - .ExecuteNonQueryAsync(reuse: true)); - } - public async Task InsertPost(PostModel post) { using (var rentedConnection = await ConnectionPool.RentConnectionAsync()) @@ -88,14 +67,29 @@ namespace BantFlags.Data.Database .ToList(); } - public async Task InsertFlagsAsync(List flags) + public async Task InsertFlagAsync(Flag flag) + { + using var rentedConnection = await ConnectionPool.RentConnectionAsync(); + await rentedConnection.Object.UseStoredProcedure("insert_flag") + .SetParam("@flag", flag.Name) + .ExecuteNonQueryAsync(); + } + + public async Task RenameFlagAsync(Flag flag) { using var rentedConnection = await ConnectionPool.RentConnectionAsync(); - using var query = rentedConnection.Object.UseStoredProcedure("insert_flag"); + await rentedConnection.Object.UseStoredProcedure("rename_flag") + .SetParam("@old", flag.OldName) + .SetParam("@new", flag.Name) + .ExecuteNonQueryAsync(); + } - flags.ForEach(async f => - await query.SetParam("@flag", f.Name) - .ExecuteNonQueryAsync(reuse: true)); + public async Task DeleteFlagAsync(Flag flag) + { + using var rentedConnection = await ConnectionPool.RentConnectionAsync(); + await rentedConnection.Object.UseStoredProcedure("delete_flag") + .SetParam("@flag", flag.Name) + .ExecuteNonQueryAsync(); } } diff --git a/BantFlags/Data/Staging.cs b/BantFlags/Data/Staging.cs index dbdc0e6..407de98 100644 --- a/BantFlags/Data/Staging.cs +++ b/BantFlags/Data/Staging.cs @@ -1,63 +1,139 @@ -using System.Collections.Generic; +using ImageMagick; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; namespace BantFlags.Data { - /// - /// Singleton class comprised of objects used in/Upload. - /// It's a fucking mess and I hate it so much. - /// public class Staging { - public List RenamedFlags { get; set; } - public List DeletedFlags { get; set; } - public List AddedFlags { get; set; } - - /// - /// The current list of resolved flags including changes currently in Staging. - /// Exists here since it's a singleton. - /// - public List Flags { get; set; } - - /// - /// Used for commiting and unstaging staged flags. - /// + public List Flags { get; set; } public string Password { get; } + public HashSet Names { get; set; } + public Staging(string password) { - RenamedFlags = new List(); - DeletedFlags = new List(); - AddedFlags = new List(); + Flags = new List(); Password = password; } public void Clear() { - RenamedFlags = new List(); - DeletedFlags = new List(); - AddedFlags = new List(); + Flags = new List(); } } - public class FormFlag + public enum Method { - public string Name { get; set; } - public bool IsChecked { get; set; } + Delete, - public Method FormMethod { get; set; } - } + Rename, - public class RenameFlag : FormFlag - { - public string NewName { get; set; } + Add } - public enum Method + public class Flag { - Delete, + public string Name { get; set; } - Rename, + public string OldName { get; set; } - Add + public bool IsChecked { get; set; } + + public Method FlagMethod { get; set; } + + public Flag() + { + } + + private Flag(string name, Method method) + { + Name = name; + FlagMethod = method; + } + + private Flag(string name, string oldName, Method method) + { + Name = name; + OldName = oldName; + FlagMethod = method; + } + + public static Result CreateFromDelete(string name) + => Result.Pass(new Flag(name, Method.Delete)); // We don't need any validation for deleted flags. + + public static Result CreateFromRename(string oldName, string newName, HashSet names) + { + Result fileName = ValidateFileName(newName, names); + + if (fileName.Failed) + return Result.Fail(fileName.ErrorMessage); + + return Result.Pass(new Flag(newName, oldName, Method.Rename)); + } + + public static async Task> CreateFromFile(IFormFile upload, HashSet names) + { + byte[] PNGHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + + if (upload.ContentType.ToLower() != "image/png") + return Result.Fail("Image must be a png."); + + if (upload.Length > 15 * 1024) + return Result.Fail("File too big. Max size is 15kb."); + + var name = Path.GetFileNameWithoutExtension(upload.FileName); + + Result fileName = ValidateFileName(name, names); + + if (fileName.Failed) + return Result.Fail(fileName.ErrorMessage); + + using (var memoryStream = new MemoryStream()) + { + await upload.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + using (var image = new MagickImage(memoryStream)) + { + if (image.Width != 16 || image.Height != 11) + return Result.Fail("Invalid image dimensions. Flags should be 16px by 11px."); + } + + using (var reader = new BinaryReader(memoryStream)) + { + reader.BaseStream.Position = 0; + + if (!reader.ReadBytes(PNGHeader.Length).SequenceEqual(PNGHeader)) + return Result.Fail("Invalid png header."); + } + } + + return Result.Pass(new Flag(name, Method.Add)); + } + + private static Result ValidateFileName(string name, HashSet names) + { + if (string.IsNullOrWhiteSpace(name)) + return Result.Fail("Flag name can't be empty."); + + if (name.Length > 100) + return Result.Fail("Flag name too long."); + + if (name == "empty, or there were errors. Re - set your flags.") + return Result.Fail("Invalid flag name."); + + if (name.Contains("||") || name.Contains(",")) + return Result.Fail("Flag name contains invalid characters. You can't use \"||\" or \",\"."); + + if (names.Contains(name)) + return Result.Fail("A flag with that name already exists."); + + return Result.Pass(name); + } } } \ No newline at end of file diff --git a/BantFlags/Pages/Index.cshtml b/BantFlags/Pages/Index.cshtml index 9bb9cdb..7dd675d 100644 --- a/BantFlags/Pages/Index.cshtml +++ b/BantFlags/Pages/Index.cshtml @@ -10,8 +10,8 @@ Install Bantflags
-
-Official Thread +
+Official Thread

Upload Flags \ No newline at end of file diff --git a/BantFlags/Pages/Upload.cshtml b/BantFlags/Pages/Upload.cshtml index 25b1833..96e52d0 100644 --- a/BantFlags/Pages/Upload.cshtml +++ b/BantFlags/Pages/Upload.cshtml @@ -1,5 +1,5 @@ @page -@using BantFlags.Data +@using BantFlags.Data @model BantFlags.UploadModel @{ ViewData["Title"] = "Upload"; @@ -8,27 +8,24 @@

Upload

-@Model.Message - +

@Model.Message

Add a Flag

- - @Html.CheckBoxFor(model => model.ShouldGloss)Apply Gloss? + + Apply Gloss?

Manage Existing Flags

-
- + -

@@ -45,77 +42,65 @@
-

Pending Changes

-
- - -
-
- - @if (Model.staging.AddedFlags.Any()) - { -

New Flags

-
- - @foreach (FormFlag flag in Model.staging.AddedFlags) +@if (Model.StagedFlags.Flags.Any()) +{ +

Pending Changes

+ + +
+
+ +

Deleted Flags

+ @* TODO: There has to be a better way to handle this*@ + @for (int i = 0; i < Model.StagedFlags.Flags.Count(); i++) + { + if (Model.StagedFlags.Flags[i].FlagMethod == Method.Delete) {
- -
- @flag.Name - - - -
- + + + + + +
} -
- } - @if (Model.staging.DeletedFlags.Any()) - { -

Deleted Flags

-
- @foreach (FormFlag flag in Model.staging.DeletedFlags) + } + +

Renamed Flags

+ @for (int i = 0; i < Model.StagedFlags.Flags.Count(); i++) + { + if (Model.StagedFlags.Flags[i].FlagMethod == Method.Rename) {
- -
- @flag.Name - - - -
- + + + + +
} -
- } + } - @if (Model.staging.RenamedFlags.Any()) - { -

Renamed Flags

-
- @foreach (RenameFlag flag in Model.staging.RenamedFlags) +

Added Flags

+ @for (int i = 0; i < Model.StagedFlags.Flags.Count(); i++) + { + if (Model.StagedFlags.Flags[i].FlagMethod == Method.Add) {
- -
- @flag.NewName - - - - -
- + + + + +
- } -
- } -
+ + } + +} @section Head { @@ -128,7 +113,7 @@ let x = document.getElementsByTagName('select')[0].children Array.prototype.slice.call(x).forEach(function (y) { var name = y.innerHTML; - y.innerHTML = "" + name + y.innerHTML = "" + name }); }, { once: true }); diff --git a/BantFlags/Pages/Upload.cshtml.cs b/BantFlags/Pages/Upload.cshtml.cs index 6780788..ca87ff9 100644 --- a/BantFlags/Pages/Upload.cshtml.cs +++ b/BantFlags/Pages/Upload.cshtml.cs @@ -7,274 +7,181 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; using System.Collections.Generic; -using System.Data; using System.IO; using System.Linq; using System.Threading.Tasks; namespace BantFlags { - // I don't know if I need these anymore. - [RequestFormLimits(ValueCountLimit = 5000)] - [IgnoreAntiforgeryToken(Order = 2000)] public class UploadModel : PageModel { - private IWebHostEnvironment Env { get; } private DatabaseService Database { get; set; } + public Staging StagedFlags { get; set; } + public string Message { get; private set; } - private readonly byte[] PNGHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; - - private string FlagsPath { get; set; } + private string WebRoot { get; } - public Staging staging { get; set; } + public HashSet AllNames => StagedFlags.Names.Concat(StagedFlags.Flags.Select(x => x.Name)).ToHashSet(); - public UploadModel(IWebHostEnvironment env, DatabaseService db, Staging s) + public UploadModel(DatabaseService dbs, Staging ns, IWebHostEnvironment env) { - Env = env; - Database = db; + Database = dbs; - staging = s; + StagedFlags = ns; - FlagsPath = Env.WebRootPath + "/flags/"; + WebRoot = env.WebRootPath; } - public string Message { get; private set; } - - // TODO: These bound properties should be inlined. - [BindProperty] - public IFormFile Upload { get; set; } - - [BindProperty] - public bool ShouldGloss { get; set; } - - public async void OnGet() + public void OnGet() { - if (staging.Flags == null) - { - staging.Flags = await Database.GetFlags(); // Because we can't populate Flags in the constructor. - } + StagedFlags.Names = StagedFlags.Names ?? Database.KnownFlags; } - public IActionResult OnPostUnstage(List addedAndDeletedFlags, List renamedFlags, string password) + public IActionResult OnPostDelete(string flag) { - if (password != staging.Password) // TODO: Maybe we should hash this? - { - Message = "Wrong Password"; - return Page(); - } - - try // Haha I can't program - { - var addedAndDeleted = addedAndDeletedFlags.Where(x => x.IsChecked); - var renamed = renamedFlags.Where(x => x.IsChecked); + var stagingFlag = Flag.CreateFromDelete(flag).Value; - addedAndDeleted.ForEach(x => - _ = x.FormMethod switch - { - // Using an enum seems kinda redundant here. - Method.Delete => staging.DeletedFlags.Remove(staging.DeletedFlags.First(y => y.Name == x.Name)), - Method.Add => staging.AddedFlags.Remove(staging.AddedFlags.First(y => y.Name == x.Name)), - _ => throw new Exception() - }); + StagedFlags.Flags.Add(stagingFlag); + StagedFlags.Names.Remove(stagingFlag.Name); - renamed.ForEach(x => staging.RenamedFlags.Remove(staging.RenamedFlags.First(y => y.Name == x.Name))); // These can be reworked to use asp-for and then we can work off the original objects. - - addedAndDeleted.ForEach(x => staging.Flags.Add(x.Name)); - renamed.ForEach(x => staging.Flags.Add(x.Name)); + Message = $"{stagingFlag.Name} deleted."; + return Page(); + } - staging.Flags = staging.Flags.Distinct().OrderBy(x => x).ToList(); + public IActionResult OnPostRename(string flag, string newName) + { + var stagingFlag = Flag.CreateFromRename(flag, newName, AllNames); - Message = $"Successfully unstaged flags"; - } - catch + if (stagingFlag.Failed) { - Message = "Something went very wrong"; + Message = stagingFlag.ErrorMessage; + return Page(); } + StagedFlags.Flags.Add(stagingFlag.Value); + StagedFlags.Names.Remove(stagingFlag.Value.OldName); + + Message = $"{stagingFlag.Value.OldName} renamed to {stagingFlag.Value.Name}."; return Page(); } - public async Task OnPostCommitAsync(string password) + public async Task OnPostAddAsync(IFormFile upload, bool gloss) { - if (password != staging.Password) + var stagingFlag = await Flag.CreateFromFile(upload, AllNames); + + if (stagingFlag.Failed) { - Message = "Wrong Password"; + Message = stagingFlag.ErrorMessage; return Page(); } - try - { - // TODO: This needs to be rewritten / burnt / both - if (staging.DeletedFlags.Any()) - { - await Database.DeleteFlagsAsync(staging.DeletedFlags); - - staging.DeletedFlags - .ForEach(flag => System.IO.File.Copy(Path.Combine(FlagsPath, flag.Name + ".png"), Path.Combine(FlagsPath, "dead/", flag.Name + ".png"))); - } - if (staging.AddedFlags.Any()) - { - await Database.InsertFlagsAsync(staging.AddedFlags); + using var memoryStream = new MemoryStream(); + await upload.CopyToAsync(memoryStream); - staging.AddedFlags - .ForEach(flag => System.IO.File.Copy(Path.Combine(FlagsPath, "staging/", flag.Name + ".png"), Path.Combine(FlagsPath, flag.Name + ".png"))); - } + memoryStream.Position = 0; - if (staging.RenamedFlags.Any()) - { - await Database.RenameFlagsAsync(staging.RenamedFlags); - - staging.RenamedFlags - .ForEach(flag => System.IO.File.Copy(Path.Combine(FlagsPath, flag.Name + ".png"), Path.Combine(FlagsPath, flag.NewName + ".png"))); - } + // Magic.NET is a huge dependency to be used like this + // Maybe we should switch to a Process and expect to have + // ImageMagick installed on the target machine. + using var image = new MagickImage(memoryStream); - await Database.UpdateKnownFlags(); - staging.Flags = await Database.GetFlags(); - staging.Clear(); - - Message = "Changes Commited successfully"; - } - catch (Exception e) + if (gloss) { - Message = "Something went bang\n" + e.Message; - } + using var glossImage = new MagickImage(WebRoot + "/gloss.png"); - return Page(); - } + glossImage.Composite(image, new PointD(0, 0), CompositeOperator.Over); + } - public IActionResult OnPostDelete(string flag) - { - staging.DeletedFlags.Add(new FormFlag - { - Name = flag - }); + image.Write(WebRoot + "/flags/staging/" + upload.FileName); - staging.Flags.Remove(flag); + StagedFlags.Flags.Add(stagingFlag.Value); + Message = $"{stagingFlag.Value.Name} uploaded"; return Page(); } - public IActionResult OnPostRename(string flag, string newName) + public IActionResult OnPostUnstage(Flag[] flags, string password) { - if (!(FileNameIsValid(newName))) + if (password != StagedFlags.Password) { - Message = "Invalid Filename."; - + Message = "Incorrect Password"; return Page(); } - staging.RenamedFlags.Add(new RenameFlag - { - Name = flag, - NewName = newName - }); - - staging.Flags.Remove(flag); - - return Page(); - } - - public async Task OnPostAddAsync() - { - try + for (int i = flags.Length - 1; i >= 0; i--) { - if (!(await ValidateImageAsync())) + if (flags[i].IsChecked != true) { - Message = "Invalid Image."; - return Page(); + continue; } - // TODO: maybe there's something no releasing memory here. - can't Directory.Move(). + StagedFlags.Flags.RemoveAt(i); - using var memoryStream = new MemoryStream(); - await Upload.CopyToAsync(memoryStream); - - memoryStream.Position = 0; - - // Magic.NET is a huge dependency to be used like this - // Maybe we should switch to a Process and expect to have - // ImageMagick installed on the target machine. - using var image = new MagickImage(memoryStream); - - if (ShouldGloss) + var flag = flags[i]; + switch (flag.FlagMethod) { - using var gloss = new MagickImage(Env.WebRootPath + "/gloss.png"); + case Method.Add: + System.IO.File.Delete(WebRoot + "/flags/staging/" + flag.Name); + StagedFlags.Names.Remove(flag.Name); + break; - gloss.Composite(image, new PointD(0, 0), CompositeOperator.Over); - } + case Method.Delete: + StagedFlags.Names.Add(flag.Name); + break; - image.Write(FlagsPath + "staging/" + Upload.FileName); + case Method.Rename: + StagedFlags.Names.Add(flag.OldName); + break; - staging.AddedFlags.Add(new FormFlag - { - Name = Path.GetFileNameWithoutExtension(Upload.FileName) - }); - - Message = "Flag uploaded successfully!"; - - return Page(); + default: + throw new Exception(); + } } - catch (Exception e) - { - Message = $"Something went bang.\n\n\n{e.Message}"; - return Page(); - } + Message = "Removed flags from staging"; + return Page(); } - // TODO: hash images and check against for duplicates. - // or is that going too far? - /// - /// Rigorously validates an image to ensure it's a flag. - /// - public async Task ValidateImageAsync() + public async Task OnPostCommit(string password) { - var fileName = Path.GetFileNameWithoutExtension(Upload.FileName); - - if (!(FileNameIsValid(fileName)) - || Upload.Length > 15 * 1024 // 15KB - || Upload.ContentType.ToLower() != "image/png") + if (password != StagedFlags.Password) { - return false; + Message = "Incorrect Password"; + return Page(); } - using (var memoryStream = new MemoryStream()) + foreach (var flag in StagedFlags.Flags) { - await Upload.CopyToAsync(memoryStream); + string flagname = flag.Name + ".png"; - memoryStream.Position = 0; - using (var image = new MagickImage(memoryStream)) + switch (flag.FlagMethod) { - if (image.Width != 16 || image.Height != 11) - { - return false; - } + case Method.Add: + await Database.InsertFlagAsync(flag); + Directory.Move(WebRoot + "/flags/staging/" + flagname, WebRoot + "/flags/" + flagname); + break; + + case Method.Delete: + await Database.DeleteFlagAsync(flag); + Directory.Move(WebRoot + "/flags/" + flagname, WebRoot + "/flags/dead/" + flagname); + break; + + case Method.Rename: + await Database.RenameFlagAsync(flag); + Directory.Move(WebRoot + "/flags/" + flag.OldName + ".png", WebRoot + "/flags/" + flagname); + break; + + default: + throw new Exception(); } + } - using (var reader = new BinaryReader(memoryStream)) - { - reader.BaseStream.Position = 0; + await Database.UpdateKnownFlags(); + StagedFlags.Names = Database.KnownFlags; + StagedFlags.Clear(); - return reader.ReadBytes(PNGHeader.Length).SequenceEqual(PNGHeader); - } - } + Message = "Changes committed successfully"; + return Page(); } - - /// - /// Matches bad things we don't want in filenames. - /// Inverts the result - returns false on a match. - /// - /// The name of the file to validate. - /// - private bool FileNameIsValid(string fileName) => - !(fileName == null - || fileName.Contains("||") - || fileName.Contains(",") - || Database.KnownFlags.Contains(fileName) - || staging.AddedFlags.Select(x => x.Name).Contains(fileName) - || staging.DeletedFlags.Select(x => x.Name).Contains(fileName) - || staging.RenamedFlags.Select(x => x.Name).Contains(fileName) - || staging.RenamedFlags.Select(x => x.NewName).Contains(fileName) - || fileName.Length > 100); } } \ No newline at end of file diff --git a/BantFlags/Startup.cs b/BantFlags/Startup.cs index cbdb561..d1b656a 100644 --- a/BantFlags/Startup.cs +++ b/BantFlags/Startup.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; diff --git a/BantFlags/wwwroot/upload.css b/BantFlags/wwwroot/upload.css index 59bcf54..6bd68e8 100644 --- a/BantFlags/wwwroot/upload.css +++ b/BantFlags/wwwroot/upload.css @@ -21,4 +21,8 @@ h3 { width: 12%; display: inline-block; padding-bottom: 10px; +} + +#message { + color: #ee0; } \ No newline at end of file