Invented a new variety of spaghetti //

Made a form for adding  and modifying flags.
dotnetflags
C-xC-c 5 years ago
parent 851337c018
commit ebb982a00b

2
.gitignore vendored

@ -32,7 +32,7 @@ bld/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
# Uncomment if you have tasks that create the project's static files in wwwroot # Uncomment if you have tasks that create the project's static files in wwwroot
/BantFlags/wwwroot/flags/* /BantFlags/wwwroot/flags/*.png
# Visual Studio 2017 auto generated files # Visual Studio 2017 auto generated files
Generated\ Files/ Generated\ Files/

@ -6,20 +6,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="7.14.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
<PackageReference Include="MySql.Data" Version="8.0.18" /> <PackageReference Include="MySql.Data" Version="8.0.18" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0" /> <PackageReference Include="Nito.AsyncEx" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\flags\" />
</ItemGroup>
</Project> </Project>

@ -3,7 +3,6 @@ using BantFlags.Data.Database;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System; using System;
using System.Collections.Generic;
using System.Data.Common; using System.Data.Common;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,36 +15,31 @@ namespace BantFlags.Controllers
{ {
private DatabaseService Database { get; } private DatabaseService Database { get; }
private string FlagList { get; set; }
private HashSet<string> DatabaseFlags { get; set; }
public FlagsController(DatabaseService db) public FlagsController(DatabaseService db)
{ {
Database = db; Database = db;
// During initialisation we get the current list of flags for resolving supported flags and preventing duplicate flags from being created
var flags = Database.GetFlags().Result; // If this fails the program should exit anyway.
FlagList = string.Join("\n", flags);
DatabaseFlags = flags.ToHashSet();
} }
/// <summary>
/// Retrives flags from the database from the posts sent in post_nrs
/// </summary>
/// <param name="post_nrs">The comma seperated list of post numbers from the thread.</param>
/// <param name="board">Currently should only be /bant/. Not checked here because we don't need to care what they send.</param>
/// <param name="version">The version of the userscript.</param>
[HttpPost] [HttpPost]
[Route("get")] [Route("get")]
[Consumes("application/x-www-form-urlencoded")] [Consumes("application/x-www-form-urlencoded")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Get([FromForm]string post_nrs, [FromForm]string board, [FromForm]string version) public async Task<IActionResult> Get([FromForm]string post_nrs, [FromForm]string board, [FromForm]int? version)
{ // TODO: version can be an int?. {
// int ver = verson ?? 0 try // We only care if the post if valid.
try
{ {
int ver = int.TryParse(version, out int x) ? x : 0; int ver = version ?? 0;
if (ver > 1) if (ver > 1)
{ {
// Improved flag sending, see Docs/GetPosts // Improved data structuring, see Docs/GetPosts
return Json(await Database.GetPosts_V2(post_nrs)); return Json(await Database.GetPosts_V2(post_nrs));
} }
else else
@ -63,15 +57,16 @@ namespace BantFlags.Controllers
/// Posts flags in the database. /// Posts flags in the database.
/// </summary> /// </summary>
/// <param name="post_nr">The post number to associate the flags to.</param> /// <param name="post_nr">The post number to associate the flags to.</param>
/// <param name="board">/bant/.</param> /// <param name="board">Currently should only be /bant/.</param>
/// <param name="regions">List of flags to associate with the post. Split by "||" in API V1 and "," in V2.</param> /// <param name="regions">List of flags to associate with the post. Split by "||" in API V1 and "," in V2.</param>
/// <param name="version">The version of the userscript.</param>
[HttpPost] [HttpPost]
[Route("post")] [Route("post")]
[Consumes("application/x-www-form-urlencoded")] [Consumes("application/x-www-form-urlencoded")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Post([FromForm]string post_nr, [FromForm]string board, [FromForm]string regions, [FromForm]int? version) public async Task<IActionResult> Post([FromForm]string post_nr, [FromForm]string board, [FromForm]string regions, [FromForm]int? version)
{ // Should we rename regions? It'd be more idomatic for it to be flags or something but then we have to introduce a new variable that does the same thing. {
try // We only care if the post if valid. try // We only care if the post if valid.
{ {
string[] flags; string[] flags;
@ -87,7 +82,20 @@ namespace BantFlags.Controllers
} }
// TODO: Currently we skip over invalid flags. Should we error instead? // TODO: Currently we skip over invalid flags. Should we error instead?
var validFlags = flags.Where(x => DatabaseFlags.Contains(x)); // We can't easily format it like in the current bantflags - we really should continue to
// return "empty, or there were errors. Re-set your flags.", for compatibility, but we'd
// have to store that as a flag in the database and perform an expensive string comparison
// to stop people selecting it.
// Do we care if people select the broken flag?
var validFlags = flags.Where(x => Database.KnownFlags().Contains(x));
for (int i = 0; i < flags.Length; i++)
{
if (!Database.KnownFlags().Contains(flags[i]))
{
flags[i] = "empty, or there were errors. Re-set your flags.";
}
}
var numberOfFlags = validFlags.Count(); var numberOfFlags = validFlags.Count();
if (numberOfFlags <= 0 || numberOfFlags > 25) if (numberOfFlags <= 0 || numberOfFlags > 25)
@ -115,7 +123,7 @@ namespace BantFlags.Controllers
[HttpGet] [HttpGet]
[Route("flags")] [Route("flags")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Flags() => Ok(FlagList); public IActionResult Flags() => Ok(Database.FlagList());
/// <summary> /// <summary>
/// Creates an error mesage to send in case of 400 bad request, without giving away too much information. /// Creates an error mesage to send in case of 400 bad request, without giving away too much information.
@ -127,7 +135,7 @@ namespace BantFlags.Controllers
NullReferenceException _ => "Some data wasn't initialised. Are you sending everything?", NullReferenceException _ => "Some data wasn't initialised. Are you sending everything?",
DbException _ => "Internal database error.", DbException _ => "Internal database error.",
ArgumentNullException _ => "No regions sent", ArgumentNullException _ => "No regions sent",
ArgumentException e => e.Message, ArgumentException e => e.Message, // We create all arguement exceptions here, so we can pass the message on.
Exception e => e.Message, // Don't do this. Exception e => e.Message, // Don't do this.
_ => "how in the hell" _ => "how in the hell"
}; // This needs more testing. }; // This needs more testing.

@ -1,4 +1,5 @@
using System.Data; using System.Collections.Generic;
using System.Data;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,9 +12,53 @@ namespace BantFlags.Data.Database
{ {
private MySqlConnectionPool ConnectionPool { get; } private MySqlConnectionPool ConnectionPool { get; }
private string Flags { get; set; }
private HashSet<string> FlagsHash { get; set; }
public DatabaseService(DatabaseServiceConfig dbConfig) public DatabaseService(DatabaseServiceConfig dbConfig)
{ {
ConnectionPool = new MySqlConnectionPool(dbConfig.ConnectionString, dbConfig.PoolSize); ConnectionPool = new MySqlConnectionPool(dbConfig.ConnectionString, dbConfig.PoolSize);
var flags = GetFlags().Result; // It's okay to error here since it's only initialised at startup.
Flags = string.Join("\n", flags);
FlagsHash = flags.ToHashSet();
}
public string FlagList() => Flags;
public HashSet<string> KnownFlags() => FlagsHash;
public async Task UpdateKnownFlags()
{
var flags = await GetFlags();
Flags = string.Join("\n", flags);
FlagsHash = flags.ToHashSet();
}
public async Task DeleteFlagsAsync(List<FormFlag> 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));
return;
}
public async Task RenameFlagsAsync(List<RenameFlag> 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(FlagModel post) public async Task InsertPost(FlagModel post)
@ -41,7 +86,7 @@ namespace BantFlags.Data.Database
/// <summary> /// <summary>
/// Returns all of the flags that we support. /// Returns all of the flags that we support.
/// </summary> /// </summary>
public async Task<EnumerableRowCollection<string>> GetFlags() public async Task<List<string>> GetFlags()
{ {
using var rentedConnected = await ConnectionPool.RentConnectionAsync(); using var rentedConnected = await ConnectionPool.RentConnectionAsync();
@ -49,7 +94,20 @@ namespace BantFlags.Data.Database
.ExecuteTableAsync(); .ExecuteTableAsync();
return table.AsEnumerable() return table.AsEnumerable()
.Select(x => x.GetValue<string>("flag")); .Select(x => x.GetValue<string>("flag"))
.ToList();
}
public async Task InsertFlagsAsync(List<FormFlag> flags)
{
using var rentedConnection = await ConnectionPool.RentConnectionAsync();
using var query = rentedConnection.Object.UseStoredProcedure("insert_flag");
flags.ForEach(async f =>
await query.SetParam("@flag", f.Name)
.ExecuteNonQueryAsync(reuse: true));
return;
} }
} }

@ -1,7 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BantFlags.Data namespace BantFlags.Data
{ {

@ -0,0 +1,56 @@
using System.Collections.Generic;
namespace BantFlags.Data
{
/// <summary>
/// Singleton class comprised of objects used in/Upload.
/// It's a fucking mess and I hate it so much.
/// </summary>
public class Staging
{
public List<RenameFlag> RenamedFlags { get; set; }
public List<FormFlag> DeletedFlags { get; set; }
public List<FormFlag> AddedFlags { get; set; }
public List<string> Flags { get; set; }
public string Password { get; }
public Staging(string password)
{
RenamedFlags = new List<RenameFlag>();
DeletedFlags = new List<FormFlag>();
AddedFlags = new List<FormFlag>();
Password = password;
}
public void Clear()
{
RenamedFlags = new List<RenameFlag>();
DeletedFlags = new List<FormFlag>();
AddedFlags = new List<FormFlag>();
}
}
public class FormFlag
{
public string Name { get; set; }
public bool IsChecked { get; set; }
public Method FormMethod { get; set; }
}
public class RenameFlag : FormFlag
{
public string NewName { get; set; }
}
public enum Method
{
Delete,
Rename,
Add
}
}

@ -6,4 +6,8 @@
} }
<h1>/bant/ Flags</h1> <h1>/bant/ Flags</h1>
<p>E<span lang="jp">メール</span>: boku (at) plum (dot) moe</p> <p>E<span lang="jp">メール</span>: boku (at) plum (dot) moe</p>
<a href="~/bantflags.user.js">Install Bantflags</a>
<br />
<a href="https://nineball.party/srsbsn/3521">Official Thread</a>

@ -1,9 +1,4 @@
using System; using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BantFlags.Pages namespace BantFlags.Pages
{ {
@ -11,7 +6,6 @@ namespace BantFlags.Pages
{ {
public void OnGet() public void OnGet()
{ {
} }
} }
} }

@ -8,5 +8,6 @@
</head> </head>
<body style="text-align: center;"> <body style="text-align: center;">
@RenderBody() @RenderBody()
@RenderSection("Scripts", required: false)
</body> </body>
</html> </html>

@ -0,0 +1,131 @@
@page
@using BantFlags.Data
@model BantFlags.UploadModel
@{
ViewData["Title"] = "Upload";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<h1>Upload</h1>
<img src="~/montage.png" />
@Model.Message
<h2>Add a Flag</h2>
<form method="post" asp-page-handler="Add" enctype="multipart/form-data">
<input type="file" asp-for="Upload" />
@Html.CheckBoxFor(model => model.ShouldGloss)<span>Apply Gloss?</span>
<br />
<br />
<input type="submit" value="Upload Flag" />
</form>
<h2>Manage Existing Flags</h2>
<form method="post" enctype="multipart/form-data">
<label>Flag:</label>
<select name="flag">
@foreach (string s in Model.staging.Flags)
{
<option>@s</option>
}
</select>
<label>New Name:</label>
<input type="text" name="newName" />
<br />
<br />
<button asp-page-handler="Delete" type="submit">Delete Flag</button>
<button asp-page-handler="Rename" type="submit">Rename Flag</button>
</form>
<h2>Commit Changes</h2>
<form method="post" asp-page-handler="Commit" enctype="multipart/form-data">
<label>Password:</label>
<input type="text" name="password" />
<br />
<br />
<input type="submit" value="Commit Changes" />
</form>
<h2>Pending Changes</h2>
<form method="post" asp-page-handler="Unstage">
<label>Password:</label>
<input type="text" name="password" />
<button type="submit" onclick="window.confirm('Really unstage the selected flags?')">unstage</button>
@if (Model.staging.AddedFlags.Any())
{
<h3>New Flags</h3>
<div class="flag-container">
@foreach (FormFlag flag in Model.staging.AddedFlags)
{
<div class="flag">
<img src="~/flags/staging/@(flag.Name).png" />
<br />
<span>@flag.Name</span>
<input type="hidden" name="addedAndDeletedFlags.Index" value="@flag.Name" />
<input type="hidden" name="addedAndDeletedFlags[@flag.Name].Name" value="@flag.Name" />
<input type="hidden" name="addedAndDeletedFlags[@flag.Name].Method" value="@Method.Add" />
<br />
<input type="checkbox" name="addedAndDeletedFlags[@flag.Name].IsChecked" value="true" />
</div>
}
</div>
}
@if (Model.staging.DeletedFlags.Any())
{
<h3>Deleted Flags</h3>
<div class="flag-container">
@foreach (FormFlag flag in Model.staging.DeletedFlags)
{
<div class="flag">
<img src="~/flags/@(flag.Name).png" />
<br />
<span>@flag.Name</span>
<input type="hidden" name="addedAndDeletedFlags.Index" value="@flag.Name" />
<input type="hidden" name="addedAndDeletedFlags[@flag.Name].Name" value="@flag.Name" />
<input type="hidden" name="addedAndDeletedFlags[@flag.Name].Method" value="@Method.Delete" />
<br />
<input type="checkbox" name="addedAndDeletedFlags[@flag.Name].IsChecked" value="true" />
</div>
}
</div>
}
@if (Model.staging.RenamedFlags.Any())
{
<h3>Renamed Flags</h3>
<div class="flag-container">
@foreach (RenameFlag flag in Model.staging.RenamedFlags)
{
<div class="flag">
<img src="~/flags/@(flag.Name).png" />
<br />
<span>@flag.NewName</span>
<input type="hidden" name="renamedFlags.Index" value="@flag.Name" />
<input type="hidden" name="renamedFlags[@flag.Name].Name" value="@flag.Name" />
<input type="hidden" name="renamedFlags[@flag.Name].Method" value="@Method.Rename" />
<input type="hidden" name="renamedFlags[@flag.Name].NewName" value="@flag.NewName" />
<br />
<input type="checkbox" name="renamedFlags[@flag.Name].IsChecked" value="true" />
</div>
}
</div>
}
</form>
@section Scripts {
@* Place flag image inside the <select> because ASP removes "invalid HTML" *@
<script>
let x = document.getElementsByTagName('select')[0].children
Array.prototype.slice.call(x).forEach(function (e) {
var name = e.innerHTML;
e.innerHTML = "<img src=\"https://flags.plum.moe/flags/" + name + ".png\">" + name
});
</script>
}
@section Head {
<link rel="stylesheet" href="~/form.css" />
}

@ -0,0 +1,275 @@
using BantFlags.Data;
using BantFlags.Data.Database;
using ImageMagick;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
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
{
[RequestFormLimits(ValueCountLimit = 5000)]
[IgnoreAntiforgeryToken(Order = 2000)]
public class UploadModel : PageModel
{
private IWebHostEnvironment Env { get; }
private DatabaseService Database { get; set; }
private readonly byte[] pngHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private string FlagsPath { get; set; }
public Staging staging { get; set; }
public UploadModel(IWebHostEnvironment env, DatabaseService db, Staging s)
{
Env = env;
Database = db;
staging = s;
FlagsPath = Env.WebRootPath + "/flags/";
}
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()
{
if (staging.Flags == null)
{
staging.Flags = await Database.GetFlags(); // Because we can't populate Flags in the constructor.
}
}
public IActionResult OnPostUnstage(List<FormFlag> addedAndDeletedFlags, List<RenameFlag> renamedFlags, string password)
{
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);
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()
});
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));
staging.Flags = staging.Flags.Distinct().OrderBy(x => x).ToList();
Message = $"Successfully unstaged flags";
}
catch
{
Message = "Something went very wrong";
}
return Page();
}
public async Task<IActionResult> OnPostCommitAsync(string password)
{
if (password != staging.Password)
{
Message = "Wrong Password";
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);
staging.AddedFlags
.ForEach(flag => System.IO.File.Copy(Path.Combine(FlagsPath, "staging/", flag.Name + ".png"), Path.Combine(FlagsPath, flag.Name + ".png")));
}
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")));
}
await Database.UpdateKnownFlags();
staging.Flags = await Database.GetFlags();
staging.Clear();
Message = "Changes Commited successfully";
}
catch (Exception e)
{
Message = "Something went bang\n" + e.Message;
}
return Page();
}
public IActionResult OnPostDelete(string flag)
{
staging.DeletedFlags.Add(new FormFlag
{
Name = flag
});
staging.Flags.Remove(flag);
return Page();
}
public IActionResult OnPostRename(string flag, string newName)
{
if (!(FileNameIsValid(newName)))
{
Message = "Invalid Filename.";
return Page();
}
staging.RenamedFlags.Add(new RenameFlag
{
Name = flag,
NewName = newName
});
staging.Flags.Remove(flag);
return Page();
}
public async Task<IActionResult> OnPostAddAsync()
{
try
{
if (!(await ValidateImageAsync()))
{
Message = "Invalid Image.";
return Page();
}
// TODO: maybe there's something no releasing memory here.
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)
{
using var gloss = new MagickImage(Env.WebRootPath + "/gloss.png");
gloss.Composite(image, new PointD(0, 0), CompositeOperator.Over);
}
image.Write(FlagsPath + "staging/" + Upload.FileName);
staging.AddedFlags.Add(new FormFlag
{
Name = Path.GetFileNameWithoutExtension(Upload.FileName)
});
Message = "Flag uploaded successfully!";
return Page();
}
catch (Exception e)
{
Message = $"Something went bang.\n{e.Message}";
return Page();
}
}
// TODO: hash images and check against for duplicates.
// or is that going too far?
/// <summary>
/// Rigorously validates an image to ensure it's a flag.
/// </summary>
public async Task<bool> ValidateImageAsync()
{
var fileName = Path.GetFileNameWithoutExtension(Upload.FileName);
if (!(FileNameIsValid(fileName))
|| Upload.Length > 15 * 1024 // 15KB
|| Upload.ContentType.ToLower() != "image/png")
{
return false;
}
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 false;
}
}
using (var reader = new BinaryReader(memoryStream))
{
reader.BaseStream.Position = 0;
return reader.ReadBytes(pngHeader.Length).SequenceEqual(pngHeader);
}
}
}
/// <summary>
/// Matches bad things we don't want in filenames.
/// Inverts the result - returns false on a match.
/// </summary>
/// <param name="fileName">The name of the file to validate.</param>
/// <returns></returns>
private bool FileNameIsValid(string fileName) =>
!(fileName == null
|| fileName.Contains("||")
|| Database.KnownFlags().Contains(fileName)
|| staging.AddedFlags.Select(x => x.Name).Contains(fileName)
|| fileName.Length > 100);
}
}

@ -0,0 +1,3 @@
@using BantFlags
@namespace BantFlags.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

@ -1,13 +1,13 @@
{ {
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": { "iisSettings": {
"windowsAuthentication": false, "windowsAuthentication": false,
"anonymousAuthentication": true, "anonymousAuthentication": true,
"iisExpress": { "iisExpress": {
"applicationUrl": "http://localhost:11673", "applicationUrl": "http://localhost:17777",
"sslPort": 44366 "sslPort": 44366
} }
}, },
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",
@ -21,10 +21,10 @@
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "weatherforecast", "launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} },
"applicationUrl": "https://localhost:5001;http://localhost:5000"
} }
} }
} }

@ -1,13 +1,16 @@
using BantFlags.Data; using BantFlags.Data;
using BantFlags.Data.Database; using BantFlags.Data.Database;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
namespace BantFlags namespace BantFlags
{ {
@ -28,7 +31,15 @@ namespace BantFlags
services.AddRazorPages(); services.AddRazorPages();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) // For image upload during production.
{
services.AddDataProtection()
.SetApplicationName("BantFlags")
.PersistKeysToFileSystem(new DirectoryInfo(@"/var/www/dotnet/bantflags/wtf-keys/"));
}
services.AddSingleton(new DatabaseService(Configuration.GetSection("dbconfig").Get<DatabaseServiceConfig>())); services.AddSingleton(new DatabaseService(Configuration.GetSection("dbconfig").Get<DatabaseServiceConfig>()));
services.AddSingleton(new Staging(Configuration.GetValue<string>("staging-password")));
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -61,8 +72,8 @@ namespace BantFlags
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers();
endpoints.MapRazorPages(); endpoints.MapRazorPages();
endpoints.MapControllers();
}); });
} }
} }

@ -13,5 +13,6 @@
} }
}, },
"webroot": "" "webroot": "/var/www/html",
"staging-password": "supersecretpassword"
} }

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

@ -0,0 +1,24 @@
body {
background: #ffe url(fade.png) top center repeat-x;
}
h1, h2, h3 {
color: maroon;
}
h3 {
clear: both;
padding-top: 20px;
}
.flag-container {
width: 60%;
margin: auto;
}
.flag {
margin: auto;
width: 12%;
display: block;
padding-bottom: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

@ -6,6 +6,7 @@ CREATE USER IF NOT EXISTS flags@localhost IDENTIFIED BY 'default';
GRANT ALL PRIVILEGES ON bantflags.* TO flags@localhost; GRANT ALL PRIVILEGES ON bantflags.* TO flags@localhost;
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
CREATE TABLE IF NOT EXISTS `flags` ( CREATE TABLE IF NOT EXISTS `flags` (
`id` INT(10) NOT NULL AUTO_INCREMENT, `id` INT(10) NOT NULL AUTO_INCREMENT,
`flag` VARCHAR(100) NOT NULL DEFAULT '0', `flag` VARCHAR(100) NOT NULL DEFAULT '0',
@ -17,6 +18,7 @@ ENGINE=InnoDB
AUTO_INCREMENT=0 AUTO_INCREMENT=0
; ;
CREATE TABLE IF NOT EXISTS `posts` ( CREATE TABLE IF NOT EXISTS `posts` (
`id` INT(10) NOT NULL AUTO_INCREMENT, `id` INT(10) NOT NULL AUTO_INCREMENT,
`post_nr` INT(10) NOT NULL DEFAULT '0', `post_nr` INT(10) NOT NULL DEFAULT '0',
@ -29,6 +31,7 @@ ENGINE=InnoDB
AUTO_INCREMENT=0 AUTO_INCREMENT=0
; ;
CREATE TABLE IF NOT EXISTS `postflags` ( CREATE TABLE IF NOT EXISTS `postflags` (
`id` INT(10) NOT NULL AUTO_INCREMENT, `id` INT(10) NOT NULL AUTO_INCREMENT,
`post_nr` INT(10) NOT NULL DEFAULT '0', `post_nr` INT(10) NOT NULL DEFAULT '0',
@ -36,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `postflags` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `flag` (`flag`), INDEX `flag` (`flag`),
INDEX `post_nr` (`post_nr`), INDEX `post_nr` (`post_nr`),
CONSTRAINT `flag` FOREIGN KEY (`flag`) REFERENCES `flags` (`id`), CONSTRAINT `flag` FOREIGN KEY (`flag`) REFERENCES `flags` (`id`) ON DELETE CASCADE,
CONSTRAINT `post_nr` FOREIGN KEY (`post_nr`) REFERENCES `posts` (`id`) CONSTRAINT `post_nr` FOREIGN KEY (`post_nr`) REFERENCES `posts` (`id`)
) )
COLLATE='utf8_general_ci' COLLATE='utf8_general_ci'
@ -44,6 +47,7 @@ ENGINE=InnoDB
AUTO_INCREMENT=0 AUTO_INCREMENT=0
; ;
DROP PROCEDURE IF EXISTS insert_post; DROP PROCEDURE IF EXISTS insert_post;
DELIMITER $$ DELIMITER $$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_post`( CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_post`(
@ -61,6 +65,7 @@ END
$$ $$
DELIMITER ; DELIMITER ;
DROP PROCEDURE IF EXISTS insert_post_flags; DROP PROCEDURE IF EXISTS insert_post_flags;
DELIMITER $$ DELIMITER $$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_post_flags`( CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_post_flags`(
@ -80,3 +85,59 @@ insert into postflags (post_nr, flag) VALUES (
END END
$$ $$
DELIMITER ; DELIMITER ;
DROP PROCEDURE IF EXISTS rename_flag;
DELIMITER $$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `rename_flag`(
IN `@old` VARCHAR(100),
IN `@new` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
UPDATE flags SET flags.flag = `@new` WHERE flags.flag = `@old`;
END
$$
DELIMITER ;
DROP PROCEDURE IF EXISTS delete_flag;
DELIMITER $$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `delete_flag`(
IN `@flag` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
DELETE flags.* FROM flags WHERE flags.flag = `@flag`;
END
$$
DELIMITER ;
DROP PROCEDURE IF EXISTS insert_flag;
DELIMITER $$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_flag`(
IN `@flag` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
INSERT INTO `flags` (`flag`) VALUES (`@flag`);
END
$$
DELIMITER ;

@ -35,6 +35,8 @@ declarations. Update your hecking browser.
- Microsoft.AspNetCore.StaticFiles - Microsoft.AspNetCore.StaticFiles
- Microsoft.EntityFrameworkCore.SqlServer - Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools - Microsoft.EntityFrameworkCore.Tools
- Microsoft.VisualStudio.Web.CodeGeneration.Design
- Magick.NET-Q8-AnyCPU
*** Setup *** Setup
1. Install .NET 1. Install .NET
@ -42,8 +44,8 @@ declarations. Update your hecking browser.
3. Create the database using [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/database.sql][database.sql]]. 3. Create the database using [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/database.sql][database.sql]].
+ *Change the password*. + *Change the password*.
4. configure =BantFlags/appsettings.example.json= with your connection 4. configure =BantFlags/appsettings.example.json= with your connection
string and webroot (the working directory / where you'll serve the string and webroot (where you'll serve the flags from *without a
flags from) and rename it to =appsettings.json= trailing /*) and rename it to =appsettings.json=
+ The location of the BantFlags application and the served content + The location of the BantFlags application and the served content
are not necessarly the same. If you leave it empty, or provide a are not necessarly the same. If you leave it empty, or provide a
nonexistant path the application will look for the =wwwroot= nonexistant path the application will look for the =wwwroot=
@ -53,9 +55,9 @@ declarations. Update your hecking browser.
=/var/www/html= and bantflags will server static content from =/var/www/html= and bantflags will server static content from
that directory, or leave =webroot= empty and bantflags will look that directory, or leave =webroot= empty and bantflags will look
for static content in =/etc/bantflags/wwwroot= for static content in =/etc/bantflags/wwwroot=
5. Add flags to the backend (currently only possible by querying the 5. Add flags to the backend either by querying the database directly
database directly), and place image *with the same name* in and placing images *with the same name* in ={webroot}/flags/= or
={webroot}/flags/=. by using /upload.
6. Configure your webserver of choice to forward requests to kestral 6. Configure your webserver of choice to forward requests to kestral
+ [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/nginx.conf][Example nginx config.]] + [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/nginx.conf][Example nginx config.]]
7. Run with =dotnet BantFlags.dll= or create a service to run it as a 7. Run with =dotnet BantFlags.dll= or create a service to run it as a
@ -66,6 +68,7 @@ declarations. Update your hecking browser.
*** Database *** Database
Tables look like this: Tables look like this:
*posts* *posts*
| id | post_nr | board | | id | post_nr | board |
| 1 | 12345 | bant | | 1 | 12345 | bant |
@ -97,4 +100,4 @@ The API is 1:1 compatable with all previous versions of bantflags, but
also encodes a new =version= variable when getting flags which allows also encodes a new =version= variable when getting flags which allows
for breaking changes in the script while the backend only sends data for breaking changes in the script while the backend only sends data
it knows is parsable. See [[https://github.com/C-xC-c/BantFlags/tree/master/Docs/][Docs/{endpoint}]] it knows is parsable. See [[https://github.com/C-xC-c/BantFlags/tree/master/Docs/][Docs/{endpoint}]]
for version compatibility. for version compatibility.
Loading…
Cancel
Save