Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • Syndrom/ClientServer
1 result
Show changes
Commits on Source (2)
......@@ -13,7 +13,7 @@ namespace ClientServer.Helpers
/// </summary>
public static class Constants
{
public static string VersionString = "2.25.2";
public static string VersionString = "2.26.0";
/// <summary>
/// this is only set once at program.cs!!
......
......@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
......@@ -2017,7 +2018,7 @@ namespace ClientServer.Controllers.Core.Exercises
if ((await _context.PLangs.FirstOrDefaultAsync(p => p.Id == solutionForBackend.PLangId)) == null)
{
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "p lang found")));
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "p lang not found")));
return;
}
......@@ -2239,6 +2240,402 @@ namespace ClientServer.Controllers.Core.Exercises
);
}
/// <summary>
/// saves or creates the user solution for the given exercise and release
///
/// we use the first plang of the participation of the user
/// we only know we get files in a zip file, so we have to match the template files by name
/// file extension must be known by the plang
///
/// see https://stackoverflow.com/questions/77718442/unable-to-send-a-multipart-request-with-an-image-and-json-using-fromform
/// <para></para>
/// IF this method returns an InvalidRequest and a string, then a file name was invalid... the string contains the invalid file name
/// </summary>
/// <param name="generatedCode">the code for the exercise release</param>
/// <param name="shouldClearAllTestResults">let the frontend decide if the code has changed... (e.g. editor has unsaved changes)</param>
/// <param name="updateSolutionData">multipart form data with a zip file as content (see type for file name)</param>
[HttpPost("save/upload/{generatedCode}/{shouldClearAllTestResults}")]
public async Task SaveCodeFilesFromUpload(string generatedCode, bool shouldClearAllTestResults,
[FromForm] SaveFilesFromZip updateSolutionData)
{
if (!await base.IsLoggedIn()) return;
int userId = GetUserId();
if (updateSolutionData == null)
{
await
Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest, "no files found")));
return;
}
//we use the first lang in the participation as plang
ExerciseReleaseWithUserAsParticipation oldExerciseReleaseWithUserAsParticipation =
await _context.ExerciseReleaseWithUserAsParticipations
.Where(p => p.ExerciseRelease.GeneratedCode == generatedCode && p.UserId == userId)
.Include(p => p.Solutions)
.ThenInclude(p => p.SolutionFiles)
.ThenInclude(solutionFile => solutionFile.TemplateFile)
.Include(p => p.Solutions)
.ThenInclude(p => p.CustomTestResults)
.Include(p => p.Solutions)
.ThenInclude(p => p.TestResults)
.Include(p => p.ExerciseRelease)
.Include(p => p.ExerciseRelease.Exercise)
.ThenInclude(exercise => exercise.CodeTemplates)
.ThenInclude(p => p.TemplateFiles)
.Include(exerciseReleaseWithUserAsParticipation =>
exerciseReleaseWithUserAsParticipation.Solutions)
.ThenInclude(solution => solution.PLang)
.FirstOrDefaultAsync()
;
ExerciseRelease release = oldExerciseReleaseWithUserAsParticipation?.ExerciseRelease;
if (oldExerciseReleaseWithUserAsParticipation?.ExerciseRelease == null)
{
//this should not happen because when accessing the exercise (getting exercise) then the participation is ensured...
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "release not found")));
return;
}
if (release.Exercise == null)
{
await
Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound,
"exercise was not found")));
return;
}
if (release.Exercise.IsPermanentlyLocked)
{
await
Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"exercise is permanently locked")));
return;
}
//check if available working time is up
if (ReleaseHelper.IsTimeUp(oldExerciseReleaseWithUserAsParticipation,
oldExerciseReleaseWithUserAsParticipation.ExerciseRelease, this))
{
return;
}
if (oldExerciseReleaseWithUserAsParticipation.ExerciseRelease.Exercise.IsOnlyVisibleToOwner)
{
//then only the owner can access the exercise
if (userId != oldExerciseReleaseWithUserAsParticipation.ExerciseRelease.Exercise.UserId)
{
await
Response.WriteAsync(
Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"exercise is only accessible by owner")));
return;
}
}
//we don't get info about the plang, use the last one
var lastEditedPLangId = oldExerciseReleaseWithUserAsParticipation.LastEditedPLangId;
//use template to get readonly files and check if youser can modify files
CodeTemplate oldCodeTemplateForPLang = release.Exercise.CodeTemplates
.FirstOrDefault(p => p.PLangId == lastEditedPLangId);
if (oldCodeTemplateForPLang == null)
{
//no template exists...
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "no template found")));
return;
}
//search for solution for the given p lang
Solution oldSolution =
oldExerciseReleaseWithUserAsParticipation.Solutions
.FirstOrDefault(
p => p.PLangId == lastEditedPLangId);
if (oldSolution == null)
{
//this should not happen because when accessing the exercise (getting exercise) then the participation (and solution) is ensured...
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "solution not found")));
return;
}
SolutionDoExerciseFullBase tmpSolution = new SolutionDoExerciseFullBase();
var tmpSolutionFiles = new List<SolutionFileDoExerciseFullBase>();
//to avoid using wrong files (e.g. osx), ignore files that start with a "." "_"
var oldSolutionPlang = oldSolution.PLang;
var validFileExtensions = oldSolutionPlang.FileExtensionsWithDot.Split(",", StringSplitOptions.RemoveEmptyEntries);
SolutionFileDoExerciseFullBase tmpMainFile = null;
var _filesCount = -1;
using (MemoryStream memoryStream = new MemoryStream())
{
await updateSolutionData.SolutionFiles.CopyToAsync(memoryStream);
try
{
using (ZipArchive zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Read))
{
for (int i = 0; i < zipArchive.Entries.Count; i++)
{
var entry = zipArchive.Entries[i];
if (string.IsNullOrEmpty(entry.Name))
{
//directory
continue;
}
// / is default but some apps use file system separator
if (entry.FullName.StartsWith(".")
|| entry.FullName.StartsWith("_")
|| entry.Name.StartsWith(".")
|| entry.Name.StartsWith("_")
)
{
//only use first level, not inside dirs
continue;
}
if (!validFileExtensions.Any(p => entry.Name.EndsWith(p)))
{
//invalid extension, ignore file
continue;
}
using StreamReader stream = new StreamReader(entry.Open(), Encoding.Default);
string fileContent = await stream.ReadToEndAsync();
string fileName = entry.Name.Trim();
TemplateFile templateFile =
oldCodeTemplateForPLang.TemplateFiles.FirstOrDefault(p =>
p.FileNameWithExtension.Trim() == fileName);
var tmpSolutionFile = new SolutionFileDoExerciseFullBase()
{
Id = _filesCount--,
Content = fileContent,
FileNameWithExtension = fileName,
TemplateFileId = null,
};
var oldSolutionFile = oldSolution.SolutionFiles.FirstOrDefault(p =>
p.FileNameWithExtension.Trim() == fileName);
if (oldSolutionFile != null)
{
//old file
tmpSolutionFile.Id = oldSolutionFile.Id;
//this is ok to set here because OverwriteSolution
//also uses the template from the old solution
templateFile = oldSolutionFile.TemplateFile;
}
if (templateFile == null)
{
//new file
//check if the user was allowed to create files
if (release.Exercise.CanUserCreateFiles)
{
if (Files.ValidFileName(fileName) == false)
{
//not a valid file name...
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"invalid filename: " + fileName.Trim())));
return;
}
}
else
{
//ignore file because user was not allowed to create...
continue;
}
}
else
{
//old file (has template)
tmpSolutionFile.TemplateFileId = templateFile.Id;
tmpSolutionFile.IsContentVisibleForUser = templateFile.IsContentVisibleForUser;
tmpSolutionFile.IsEditableByUser = templateFile.IsEditableByUser;
tmpSolutionFile.UiIsDisplayed = true;
if (oldCodeTemplateForPLang.MainFileId == templateFile.Id)
{
tmpMainFile = tmpSolutionFile;
}
}
tmpSolutionFiles.Add(tmpSolutionFile);
}
}
}
catch (Exception e)
{
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"no or invalid zip provided")));
return;
}
}
if (tmpSolutionFiles.Count == 0)
{
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"no files found")));
return;
}
if (tmpMainFile == null)
{
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"main file missing")));
return;
}
tmpSolution.MainFileId = tmpMainFile.Id;
tmpSolution.SolutionFiles = tmpSolutionFiles;
List<Tuple<SolutionFileDoExerciseFullBase, SolutionFile>> fileAssociation = null;
Tuple<Solution, SolutionFile> mainClassFileTuple = null;
#region handle solution update
string invalidFileName = OverwriteSolution(tmpSolution,
oldSolution, oldCodeTemplateForPLang,
release, shouldClearAllTestResults, ref mainClassFileTuple, out fileAssociation);
//invalidate
tmpSolution = null;
if (invalidFileName != null)
{
await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
"invalid filename: " + invalidFileName)));
return;
}
oldSolution.LastEditingIpAddress = NetworkHelper.GetIpAddress(HttpContext);
oldExerciseReleaseWithUserAsParticipation.LastUpdatedAt = DateTimeHelper.GetUtcNow();
#endregion
//check if main file really got set
if (mainClassFileTuple == null)
{
await
Response.WriteAsync(
Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "no main file found")));
return;
}
try
{
using (var transaction = _context.Database.BeginTransaction())
{
try
{
//use a transaction to ensure the main file is already
//save the solution changes
await _context.SaveChangesAsync();
//always save in case user has an invalid solution
mainClassFileTuple.Item1.MainFile = mainClassFileTuple.Item2;
await _context.SaveChangesAsync();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
await base.HandleDbError(ex);
return;
}
}
}
catch (Exception e)
{
//see https://www.npgsql.org/doc/transactions.html
await base.HandleDbError(e);
return;
}
string errorMsg =
UserSolutionHelper.ReplaceReadonlyFiles(oldSolution, oldCodeTemplateForPLang, true, false);
if (string.IsNullOrEmpty(errorMsg) == false)
{
await
Response.WriteAsync(
Jc.Serialize(new BasicResponse(ResponseCode.ServerError,
"could not replace readonly or hiden files")));
return;
}
var solutionVersionForFrontend = new SolutionDoExerciseFullBase()
{
UserId = oldSolution.UserId,
ReleaseId = oldSolution.ExerciseReleaseId,
PLangId = oldSolution.PLangId,
LastUpdatedAt = oldSolution.LastUpdatedAt,
// ReSharper disable once PossibleInvalidOperationException //this should be set or checked in OverwriteSolutionVersion or SaveNewSolutionVersion
MainFileId = oldSolution.MainFileId ?? 0,
SolutionFiles =
oldSolution.SolutionFiles.Select(
p => new SolutionFileDoExerciseFullBase() //solutionVersionForBackend
{
Id = p.Id,
Content = p.Content,
UiIsDisplayed = p.IsDisplayed,
//when there is no template file the user created the file and is allowed to edit it
IsEditableByUser = (p.TemplateFile == null || p.TemplateFile.IsEditableByUser),
FileNameWithExtension = p.FileNameWithExtension,
TemplateFileId = p.TemplateFileId,
DisplayIndex = p.DisplayIndex,
IsContentVisibleForUser = p.TemplateFile?.IsContentVisibleForUser ?? true,
}).ToList()
};
var solutionVersionTupleForFrontend = new SolutionTupleFromBackend
{
Solution = solutionVersionForFrontend,
FileIdAssociationMap = fileAssociation.Select(p => new FileIdAssociation()
{
FrontendFileId = p.Item1.Id,
BackendFileId = p.Item2.Id
}).ToList()
};
await
Response.WriteAsync(
Jc.Serialize(new BasicResponseWithData<SolutionTupleFromBackend>(ResponseCode.Ok,
"saved solution version",
solutionVersionTupleForFrontend))
);
}
[HttpPost("reset/{generatedCode}/{pLangId}")]
public async Task ResetCode(string generatedCode, int pLangId)
......@@ -4337,5 +4734,17 @@ namespace ClientServer.Controllers.Core.Exercises
#endregion
#region only for api
//see https://stackoverflow.com/questions/77718442/unable-to-send-a-multipart-request-with-an-image-and-json-using-fromform
public class SaveFilesFromZip
{
[FromForm(Name = "solutionFiles")]
public IFormFile SolutionFiles { get; set; }
}
#endregion
#endregion
}
......@@ -261,12 +261,13 @@ namespace ClientServer
{
// Console.WriteLine(e);
context.Response.ContentType = "application/json";
context.Response.StatusCode = 200;
context.Response.StatusCode = 500;
await
context.Response.WriteAsync(
Jc.Serialize(
new BasicResponse(ResponseCode.ServerError, $"file content size (multipart body) exceeded the limit of " +
$"{AppConfiguration.MaxUploadFileSizeInByte} Bytes (~{Files.GetHumanReadableSize(AppConfiguration.MaxUploadFileSizeInByte)})")));
new BasicResponse(ResponseCode.ServerError,
$"unknown exception or file content size (multipart body) exceeded the limit of " +
$"{AppConfiguration.MaxUploadFileSizeInByte} Bytes (~{Files.GetHumanReadableSize(AppConfiguration.MaxUploadFileSizeInByte)})")));
return;
}
......