diff --git a/.gitignore b/.gitignore index 39315cf..d96909d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ Docker/*.class Docker/*.jar TestingInput TestingSource +TestingInclude sqldialects.xml contentModel.xml workspace.xml diff --git a/Controllers/HTMLController.cs b/Controllers/HTMLController.cs index 9d2a88d..2e33fe5 100644 --- a/Controllers/HTMLController.cs +++ b/Controllers/HTMLController.cs @@ -73,7 +73,7 @@ public virtual async Task Get(string page) } } - private static string GetMIME(string file) + public static string GetMIME(string file) { var provider = new FileExtensionContentTypeProvider(); return !provider.TryGetContentType(file, out var contentType) ? "application/octet-stream" : contentType; diff --git a/Controllers/MySqlQuery.cs b/Controllers/MySqlQuery.cs index b03050b..441874e 100644 --- a/Controllers/MySqlQuery.cs +++ b/Controllers/MySqlQuery.cs @@ -91,9 +91,9 @@ public async Task AddTest(string name, IEnumerable testers, IEnumer /// An integer, representing the ID of the Stipulatable. public async Task AddTest(string name, IEnumerable testers, IEnumerable required, string description, IEnumerable included) { - const string commandMySql = "INSERT INTO Modulr.Stipulatables (`name`, testers, required) VALUES (@Name, @Testers, @Required); SELECT LAST_INSERT_ID();"; + const string commandMySql = "INSERT INTO Modulr.Stipulatables (`name`, testers, required, provided, description) VALUES (@Name, @Testers, @Required, @Provided, @Description); SELECT LAST_INSERT_ID();"; var results = await Connection.QuerySingleOrDefaultAsync(ConvertSql(commandMySql), - new {Name = name, Testers = JsonConvert.SerializeObject(testers), Required = JsonConvert.SerializeObject(required)}); + new { Name = name, Testers = JsonConvert.SerializeObject(testers), Required = JsonConvert.SerializeObject(required), Provided = JsonConvert.SerializeObject(included), Description = description }); return results; } @@ -109,10 +109,10 @@ public async Task UpdateTest(int id, string name, IEnumerable test { const string commandMySql = "UPDATE Modulr.Stipulatables SET `name` = @Name, testers = @Testers, required = @Required WHERE id = @ID"; return await Connection.ExecuteAsync(ConvertSql(commandMySql), - new {Name = name, + new { Name = name, Testers = JsonConvert.SerializeObject(testers), Required = JsonConvert.SerializeObject(required), - ID = id}) != 0; + ID = id }) != 0; } /// diff --git a/Controllers/TestController.cs b/Controllers/TestController.cs index 24c6ece..ea1b405 100644 --- a/Controllers/TestController.cs +++ b/Controllers/TestController.cs @@ -116,6 +116,34 @@ public async Task FileUpload([FromForm] TesterFiles input) await _query.DecrementAttempts(user.Subject); return output; } + + /// + /// Allow a user to download a provided file. + /// + /// A file requested from the user. + /// Data representing the file, or an error message. + [HttpPost("Download")] + public async Task FileDownload([FromBody] DownloadFile file) + { + if (file.File == null) + return BadRequest("Bad Filename!!"); + + var (status, _) = await _auth.Verify(file.AuthToken); + if (status != GoogleAuth.LoginStatus.Success) + return Forbid(); + + var test = await _query.GetTest(file.TestID); + if (test == null) + return NotFound("Failed to find Test ID!"); + + var fileName = Path.GetFileName(file.File); + var path = Path.Combine(Path.Join(_config.IncludeLocation, "" + file.TestID), fileName!); + if (!System.IO.File.Exists(path)) + return NotFound("File not found!"); + + var stream = System.IO.File.OpenRead(path); + return new FileStreamResult(stream, HTMLController.GetMIME(fileName)); + } /// /// A simple way to fail with a HTTP status. diff --git a/Models/AdminStipulatable.cs b/Models/AdminStipulatable.cs index 8cf044d..947de06 100644 --- a/Models/AdminStipulatable.cs +++ b/Models/AdminStipulatable.cs @@ -17,8 +17,8 @@ public class AdminStipulatable public bool Valid { get; set; } public IReadOnlyCollection TesterFiles => _testerFiles; public IReadOnlyCollection RequiredFiles => _requiredFiles; - private readonly List _testerFiles = new List(); - private readonly List _requiredFiles = new List(); + private readonly List _testerFiles = new(); + private readonly List _requiredFiles = new(); public AdminStipulatable(Stipulatable sp) { diff --git a/Models/DownloadFile.cs b/Models/DownloadFile.cs new file mode 100644 index 0000000..f5fd031 --- /dev/null +++ b/Models/DownloadFile.cs @@ -0,0 +1,8 @@ +namespace Modulr.Models +{ + public class DownloadFile : BasicAuth + { + public int TestID { get; set; } + public string File { get; set; } + } +} \ No newline at end of file diff --git a/Models/TestQuery.cs b/Models/TestQuery.cs index cff3982..4ee4be1 100644 --- a/Models/TestQuery.cs +++ b/Models/TestQuery.cs @@ -1,8 +1,7 @@ namespace Modulr.Models { - public class TestQuery + public class TestQuery : BasicAuth { - public string AuthToken { get; set; } public int TestID { get; set; } } } \ No newline at end of file diff --git a/Modulr.csproj b/Modulr.csproj index 7cb3597..6318a96 100644 --- a/Modulr.csproj +++ b/Modulr.csproj @@ -10,6 +10,7 @@ Always + @@ -29,4 +30,16 @@ + + + + + + + + + + + + diff --git a/Program.cs b/Program.cs index fb7b687..b38a01e 100644 --- a/Program.cs +++ b/Program.cs @@ -4,7 +4,7 @@ namespace Modulr { - public class Program + public static class Program { public static void Main(string[] args) { diff --git a/Startup.cs b/Startup.cs index fafec7c..da1e5c1 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Builder; diff --git a/StaticViews/js/tester.js b/StaticViews/js/tester.js index 48d478e..bfdf18d 100644 --- a/StaticViews/js/tester.js +++ b/StaticViews/js/tester.js @@ -219,6 +219,7 @@ async function getAllTests() { handleErrors(0, e); } } + async function getAttemptsLeft() { try { let response = await fetch("/Users/GetTimeout", { @@ -299,11 +300,57 @@ function generateProvided(provided) { let providedList = providedArea.querySelector(".center"); providedList.innerHTML = ""; for (let file of provided) { - let link = document.createElement("a"); - link.href = "javascript:alert('Download link for " + file + "')"; - link.className = "input normal"; - link.innerHTML = file; - providedList.appendChild(link); + // + let button = document.createElement("button"); + button.className = "input normal"; + button.innerHTML = file; + button.addEventListener("click", async (e) => { + e.preventDefault(); + await downloadFile(file); + }); + providedList.appendChild(button); + } +} + +async function downloadFile(file) { + try { + let response = await fetch("/Tester/Download", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "AuthToken": getLoginToken(), + "TestID": currentTest, + "File": file + }) + }); + if (response.status >= 400 && response.status < 600) { + handleErrors(response.status, null); + } else { + // What's compatibility? And can I eat it? + const newBlob = new Blob([ await response.blob() ], { type: response.headers.get("Content-Type") }); + + if (window.navigator && window.navigator.msSaveOrOpenBlob) + window.navigator.msSaveOrOpenBlob(newBlob); + else { + const data = URL.createObjectURL(newBlob); + /* const newPanel = open(data, "_blank"); + if (newPanel !== null) + newPanel.focus(); */ + + let link = document.createElement("a"); + link.href = data; + link.download = file; + link.click(); + + // I saw this somewhere, apparently for Firefox. + setTimeout(() => { URL.revokeObjectURL(data); }, 250); + } + } + } + catch (e) { + handleErrors(0, e); } } diff --git a/Tester/ModulrConfig.cs b/Tester/ModulrConfig.cs index e35323c..bf18c2b 100644 --- a/Tester/ModulrConfig.cs +++ b/Tester/ModulrConfig.cs @@ -21,6 +21,7 @@ public class ModulrConfig [JsonProperty] public bool AutoUpdateDockerImage { get; private set; } [JsonProperty] public string SaveLocation { get; private set; } [JsonProperty] public string SourceLocation { get; private set; } + [JsonProperty] public string IncludeLocation { get; private set; } [JsonProperty] public string SqlServer { get; private set; } [JsonProperty] public int SqlPort { get; private set; } [JsonProperty] public string SqlPassword { get; private set; } @@ -95,6 +96,10 @@ private void SetDefaultConfig() SqlServer = "localhost"; UseMySql = true; TimeoutAttempts = -1; + + SourceLocation = "TestingSource"; + SaveLocation = "TestingInput"; + IncludeLocation = "TestingInclude"; } private readonly string[] _dockerWinPath = { diff --git a/Tester/ModulrJail.cs b/Tester/ModulrJail.cs index 2487725..3a370f3 100644 --- a/Tester/ModulrJail.cs +++ b/Tester/ModulrJail.cs @@ -12,7 +12,7 @@ namespace Modulr.Tester /// public abstract class ModulrJail : IDisposable { - private static readonly string MODULR_STIPULATOR_GITHUB = "https://github.com/DoggySazHi/Modulr.Stipulator/releases/latest/download/Modulr.Stipulator.jar"; + private const string MODULR_STIPULATOR_GITHUB = "https://github.com/DoggySazHi/Modulr.Stipulator/releases/latest/download/Modulr.Stipulator.jar"; public static ModulrConfig Config { private protected get; set; } public static TestWorker WebSocket { private protected get; set; }