Fixes and improvements from Operation Purge (#267)

* Fixes and improvements from Operation Purge

* Limit level name and description length

* Implement suggestions from code review

Co-authored-by: jvyden <jvyden@jvyden.xyz>
This commit is contained in:
Josh 2022-04-06 17:26:40 -05:00 committed by GitHub
commit 8ef8c204a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 243 additions and 93 deletions

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
@ -78,16 +79,18 @@ public class CommentController : ControllerBase
[HttpPost("postComment/user/{slotId:int}")] [HttpPost("postComment/user/{slotId:int}")]
public async Task<IActionResult> PostComment(string? username, int? slotId) public async Task<IActionResult> PostComment(string? username, int? slotId)
{ {
User? poster = await this.database.UserFromGameRequest(this.Request);
if (poster == null) return this.StatusCode(403, "");
this.Request.Body.Position = 0; this.Request.Body.Position = 0;
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();
XmlSerializer serializer = new(typeof(Comment)); XmlSerializer serializer = new(typeof(Comment));
Comment? comment = (Comment?)serializer.Deserialize(new StringReader(bodyString)); Comment? comment = (Comment?)serializer.Deserialize(new StringReader(bodyString));
CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level); SanitizationHelper.SanitizeStringsInClass(comment);
User? poster = await this.database.UserFromGameRequest(this.Request); CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level);
if (poster == null) return this.StatusCode(403, "");
if (comment == null) return this.BadRequest(); if (comment == null) return this.BadRequest();

View file

@ -37,6 +37,8 @@ public class FriendsController : ControllerBase
NPData? npData = (NPData?)serializer.Deserialize(new StringReader(bodyString)); NPData? npData = (NPData?)serializer.Deserialize(new StringReader(bodyString));
if (npData == null) return this.BadRequest(); if (npData == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(npData);
List<User> friends = new(); List<User> friends = new();
foreach (string friendName in npData.Friends) foreach (string friendName in npData.Friends)
{ {

View file

@ -36,6 +36,8 @@ public class ReportController : ControllerBase
if (report == null) return this.BadRequest(); if (report == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(report);
report.Bounds = JsonSerializer.Serialize(report.XmlBounds.Rect, typeof(Rectangle)); report.Bounds = JsonSerializer.Serialize(report.XmlBounds.Rect, typeof(Rectangle));
report.Players = JsonSerializer.Serialize(report.XmlPlayers, typeof(ReportPlayer[])); report.Players = JsonSerializer.Serialize(report.XmlPlayers, typeof(ReportPlayer[]));
report.Timestamp = TimeHelper.UnixTimeMilliseconds(); report.Timestamp = TimeHelper.UnixTimeMilliseconds();

View file

@ -44,6 +44,8 @@ public class PhotosController : ControllerBase
Photo? photo = (Photo?)serializer.Deserialize(new StringReader(bodyString)); Photo? photo = (Photo?)serializer.Deserialize(new StringReader(bodyString));
if (photo == null) return this.BadRequest(); if (photo == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(photo);
foreach (Photo p in this.database.Photos.Where(p => p.CreatorId == user.UserId)) foreach (Photo p in this.database.Photos.Where(p => p.CreatorId == user.UserId))
{ {
if (p.LargeHash == photo.LargeHash) return this.Ok(); // photo already uplaoded if (p.LargeHash == photo.LargeHash) return this.Ok(); // photo already uplaoded

View file

@ -30,7 +30,7 @@ public class ResourcesController : ControllerBase
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();
XmlSerializer serializer = new(typeof(ResourceList)); XmlSerializer serializer = new(typeof(ResourceList));
ResourceList resourceList = (ResourceList)serializer.Deserialize(new StringReader(bodyString)); ResourceList? resourceList = (ResourceList?)serializer.Deserialize(new StringReader(bodyString));
if (resourceList == null) return this.BadRequest(); if (resourceList == null) return this.BadRequest();

View file

@ -85,7 +85,14 @@ public class PublishController : ControllerBase
User user = userAndToken.Value.Item1; User user = userAndToken.Value.Item1;
GameToken gameToken = userAndToken.Value.Item2; GameToken gameToken = userAndToken.Value.Item2;
Slot? slot = await this.getSlotFromBody(); Slot? slot = await this.getSlotFromBody();
if (slot?.Location == null) return this.BadRequest();
if (slot == null) return this.BadRequest();
if (slot.Location == null) return this.BadRequest();
if (slot.Description.Length > 200) return this.BadRequest();
if (slot.Name.Length > 100) return this.BadRequest();
foreach (string resource in slot.Resources) foreach (string resource in slot.Resources)
{ {
@ -219,6 +226,8 @@ public class PublishController : ControllerBase
XmlSerializer serializer = new(typeof(Slot)); XmlSerializer serializer = new(typeof(Slot));
Slot? slot = (Slot?)serializer.Deserialize(new StringReader(bodyString)); Slot? slot = (Slot?)serializer.Deserialize(new StringReader(bodyString));
SanitizationHelper.SanitizeStringsInClass(slot);
return slot; return slot;
} }

View file

@ -41,10 +41,12 @@ public class ReviewController : ControllerBase
RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId); RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId);
if (ratedLevel == null) if (ratedLevel == null)
{ {
ratedLevel = new RatedLevel(); ratedLevel = new RatedLevel
ratedLevel.SlotId = slotId; {
ratedLevel.UserId = user.UserId; SlotId = slotId,
ratedLevel.Rating = 0; UserId = user.UserId,
Rating = 0,
};
this.database.RatedLevels.Add(ratedLevel); this.database.RatedLevels.Add(ratedLevel);
} }
@ -68,10 +70,12 @@ public class ReviewController : ControllerBase
RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId); RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId);
if (ratedLevel == null) if (ratedLevel == null)
{ {
ratedLevel = new RatedLevel(); ratedLevel = new RatedLevel
ratedLevel.SlotId = slotId; {
ratedLevel.UserId = user.UserId; SlotId = slotId,
ratedLevel.RatingLBP1 = 0; UserId = user.UserId,
RatingLBP1 = 0,
};
this.database.RatedLevels.Add(ratedLevel); this.database.RatedLevels.Add(ratedLevel);
} }
@ -91,18 +95,23 @@ public class ReviewController : ControllerBase
User? user = await this.database.UserFromGameRequest(this.Request); User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, ""); if (user == null) return this.StatusCode(403, "");
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
Review? newReview = await this.getReviewFromBody(); Review? newReview = await this.getReviewFromBody();
if (newReview == null) return this.BadRequest(); if (newReview == null) return this.BadRequest();
if (newReview.Text.Length > 100) return this.BadRequest();
Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId);
if (review == null) if (review == null)
{ {
review = new Review(); review = new Review
review.SlotId = slotId; {
review.ReviewerId = user.UserId; SlotId = slotId,
review.DeletedBy = DeletedBy.None; ReviewerId = user.UserId,
review.ThumbsUp = 0; DeletedBy = DeletedBy.None,
review.ThumbsDown = 0; ThumbsUp = 0,
ThumbsDown = 0,
};
this.database.Reviews.Add(review); this.database.Reviews.Add(review);
} }
review.Thumb = newReview.Thumb; review.Thumb = newReview.Thumb;
@ -115,10 +124,12 @@ public class ReviewController : ControllerBase
RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId); RatedLevel? ratedLevel = await this.database.RatedLevels.FirstOrDefaultAsync(r => r.SlotId == slotId && r.UserId == user.UserId);
if (ratedLevel == null) if (ratedLevel == null)
{ {
ratedLevel = new RatedLevel(); ratedLevel = new RatedLevel
ratedLevel.SlotId = slotId; {
ratedLevel.UserId = user.UserId; SlotId = slotId,
ratedLevel.RatingLBP1 = 0; UserId = user.UserId,
RatingLBP1 = 0,
};
this.database.RatedLevels.Add(ratedLevel); this.database.RatedLevels.Add(ratedLevel);
} }
@ -149,12 +160,14 @@ public class ReviewController : ControllerBase
.Where(r => r.SlotId == slotId) .Where(r => r.SlotId == slotId)
.Include(r => r.Reviewer) .Include(r => r.Reviewer)
.Include(r => r.Slot) .Include(r => r.Slot)
.OrderByDescending(r => r.ThumbsUp) .OrderByDescending(r => r.ThumbsUp - r.ThumbsDown)
.ThenByDescending(r => r.Timestamp) .ThenByDescending(r => r.Timestamp)
.Skip(pageStart - 1) .Skip(pageStart - 1)
.Take(pageSize); .Take(pageSize);
string inner = reviews.ToList() List<Review?> reviewList = reviews.ToList();
string inner = reviewList
.Aggregate .Aggregate
( (
string.Empty, string.Empty,
@ -162,10 +175,10 @@ public class ReviewController : ControllerBase
{ {
if (review == null) return current; if (review == null) return current;
return current + review.Serialize(); RatedReview? yourThumb = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
return current + review.Serialize(null, yourThumb);
} }
); );
string response = LbpSerializer.TaggedStringElement string response = LbpSerializer.TaggedStringElement
( (
"reviews", "reviews",
@ -176,7 +189,7 @@ public class ReviewController : ControllerBase
"hint_start", pageStart + pageSize "hint_start", pageStart + pageSize
}, },
{ {
"hint", pageStart // not sure "hint", reviewList.LastOrDefault()!.Timestamp // not sure
}, },
} }
); );
@ -197,22 +210,24 @@ public class ReviewController : ControllerBase
GameVersion gameVersion = gameToken.GameVersion; GameVersion gameVersion = gameToken.GameVersion;
IEnumerable<Review?> reviews = this.database.Reviews.ByGameVersion(gameVersion, true) IEnumerable<Review?> reviews = this.database.Reviews.ByGameVersion(gameVersion, true)
.Where(r => r.Reviewer.Username == username)
.Include(r => r.Reviewer) .Include(r => r.Reviewer)
.Include(r => r.Slot) .Include(r => r.Slot)
.Where(r => r.Reviewer!.Username == username)
.OrderByDescending(r => r.Timestamp) .OrderByDescending(r => r.Timestamp)
.Skip(pageStart - 1) .Skip(pageStart - 1)
.Take(pageSize) .Take(pageSize);
.AsEnumerable();
string inner = reviews.Aggregate List<Review?> reviewList = reviews.ToList();
string inner = reviewList.Aggregate
( (
string.Empty, string.Empty,
(current, review) => (current, review) =>
{ {
//RatedLevel? ratedLevel = this.database.RatedLevels.FirstOrDefault(r => r.SlotId == review.SlotId && r.UserId == user.UserId); if (review == null) return current;
//RatedReview? ratedReview = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
return current + review.Serialize( /*, ratedReview*/); RatedReview? ratedReview = this.database.RatedReviews.FirstOrDefault(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
return current + review.Serialize(null, ratedReview);
} }
); );
@ -226,7 +241,7 @@ public class ReviewController : ControllerBase
"hint_start", pageStart "hint_start", pageStart
}, },
{ {
"hint", reviews.Last().Timestamp // Seems to be the timestamp of oldest "hint", reviewList.LastOrDefault()!.Timestamp // Seems to be the timestamp of oldest
}, },
} }
); );
@ -249,25 +264,40 @@ public class ReviewController : ControllerBase
RatedReview? ratedReview = await this.database.RatedReviews.FirstOrDefaultAsync(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId); RatedReview? ratedReview = await this.database.RatedReviews.FirstOrDefaultAsync(r => r.ReviewId == review.ReviewId && r.UserId == user.UserId);
if (ratedReview == null) if (ratedReview == null)
{ {
ratedReview = new RatedReview(); ratedReview = new RatedReview
ratedReview.ReviewId = review.ReviewId; {
ratedReview.UserId = user.UserId; ReviewId = review.ReviewId,
ratedReview.Thumb = 0; UserId = user.UserId,
Thumb = 0,
};
this.database.RatedReviews.Add(ratedReview); this.database.RatedReviews.Add(ratedReview);
await this.database.SaveChangesAsync();
} }
int oldThumb = ratedReview.Thumb; int oldRating = ratedReview.Thumb;
ratedReview.Thumb = Math.Max(Math.Min(1, rating), -1); ratedReview.Thumb = Math.Clamp(rating, -1, 1);
if (oldRating == ratedReview.Thumb) return this.Ok();
if (oldThumb != ratedReview.Thumb) // if the user's rating changed then we recount the review's ratings to ensure accuracy
List<RatedReview> reactions = await this.database.RatedReviews.Where(r => r.ReviewId == review.ReviewId).ToListAsync();
int yay = 0;
int boo = 0;
foreach (RatedReview r in reactions)
{ {
if (oldThumb == -1) review.ThumbsDown--; switch (r.Thumb)
else if (oldThumb == 1) review.ThumbsUp--; {
case -1:
if (ratedReview.Thumb == -1) review.ThumbsDown++; boo++;
else if (ratedReview.Thumb == 1) review.ThumbsUp++; break;
case 1:
yay++;
break;
}
} }
review.ThumbsDown = boo;
review.ThumbsUp = yay;
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
return this.Ok(); return this.Ok();
@ -296,7 +326,7 @@ public class ReviewController : ControllerBase
XmlSerializer serializer = new(typeof(Review)); XmlSerializer serializer = new(typeof(Review));
Review? review = (Review?)serializer.Deserialize(new StringReader(bodyString)); Review? review = (Review?)serializer.Deserialize(new StringReader(bodyString));
SanitizationHelper.SanitizeStringsInClass(review);
return review; return review;
} }
} }

View file

@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
@ -43,6 +44,8 @@ public class ScoreController : ControllerBase
Score? score = (Score?)serializer.Deserialize(new StringReader(bodyString)); Score? score = (Score?)serializer.Deserialize(new StringReader(bodyString));
if (score == null) return this.BadRequest(); if (score == null) return this.BadRequest();
SanitizationHelper.SanitizeStringsInClass(score);
score.SlotId = id; score.SlotId = id;
Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId); Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId);

View file

@ -6,6 +6,7 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
@ -93,7 +94,19 @@ public class UserController : ControllerBase
if (update == null) return this.BadRequest(); if (update == null) return this.BadRequest();
if (update.Biography != null) user.Biography = update.Biography; SanitizationHelper.SanitizeStringsInClass(update);
if (update.Biography != null)
{
if (update.Biography.Length > 100) return this.BadRequest();
user.Biography = update.Biography;
}
foreach (string? resource in new[] {update.IconHash, update.YayHash, update.MehHash, update.BooHash, update.PlanetHash,})
{
if (resource != null && !FileHelper.ResourceExists(resource)) return this.BadRequest();
}
if (update.IconHash != null) user.IconHash = update.IconHash; if (update.IconHash != null) user.IconHash = update.IconHash;

View file

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using System.Threading.Tasks; using System.Threading.Tasks;
using Kettu; using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
@ -48,6 +49,8 @@ public class SlotPageController : ControllerBase
return this.Redirect("~/slot/" + id); return this.Redirect("~/slot/" + id);
} }
msg = SanitizationHelper.SanitizeString(msg);
await this.database.PostComment(user, id, CommentType.Level, msg); await this.database.PostComment(user, id, CommentType.Level, msg);
Logger.Log($"Posted comment from {user.UserId}: \"{msg}\" on user {id}", LoggerLevelComments.Instance); Logger.Log($"Posted comment from {user.UserId}: \"{msg}\" on user {id}", LoggerLevelComments.Instance);

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Kettu; using Kettu;
using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Helpers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -42,6 +43,8 @@ public class UserPageController : ControllerBase
return this.Redirect("~/user/" + id); return this.Redirect("~/user/" + id);
} }
msg = SanitizationHelper.SanitizeString(msg);
await this.database.PostComment(user, id, CommentType.Profile, msg); await this.database.PostComment(user, id, CommentType.Profile, msg);
Logger.Log($"Posted comment from {user.UserId}: \"{msg}\" on user {id}", LoggerLevelComments.Instance); Logger.Log($"Posted comment from {user.UserId}: \"{msg}\" on user {id}", LoggerLevelComments.Instance);

View file

@ -138,6 +138,8 @@ public class Database : DbContext
reaction = newReaction; reaction = newReaction;
} }
rating = Math.Clamp(rating, -1, 1);
int oldRating = reaction.Rating; int oldRating = reaction.Rating;
if (oldRating == rating) return true; if (oldRating == rating) return true;

View file

@ -0,0 +1,43 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace LBPUnion.ProjectLighthouse.Helpers;
public static class SanitizationHelper
{
private static readonly Dictionary<string, string> charsToReplace = new() {
{"<", "&lt;"},
{">", "&gt;"},
};
public static void SanitizeStringsInClass(object? instance)
{
if (instance == null) return;
PropertyInfo[] properties = instance.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
if (property.PropertyType != typeof(string)) continue;
string? before = (string?) property.GetValue(instance);
if (before == null) continue;
if (!charsToReplace.Keys.Any(k => before.Contains(k))) continue;
property.SetValue(instance, SanitizeString(before));
}
}
public static string SanitizeString(string input)
{
foreach ((string? key, string? value) in charsToReplace)
{
input = input.Replace(key, value);
}
return input;
}
}

View file

@ -0,0 +1,36 @@
#nullable enable
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Reports;
using LBPUnion.ProjectLighthouse.Types.Reviews;
namespace LBPUnion.ProjectLighthouse.Maintenance.MaintenanceJobs;
public class CleanupXmlInjection : IMaintenanceJob
{
private readonly Database database = new();
public string Name() => "Sanitize user content";
public string Description() => "Sanitizes all user-generated strings in levels, reviews, comments, users, and scores to prevent XML injection. Only needs to be run once.";
public async Task Run()
{
foreach (Slot slot in this.database.Slots) SanitizationHelper.SanitizeStringsInClass(slot);
foreach (Review review in this.database.Reviews) SanitizationHelper.SanitizeStringsInClass(review);
foreach (Comment comment in this.database.Comments) SanitizationHelper.SanitizeStringsInClass(comment);
foreach (Score score in this.database.Scores) SanitizationHelper.SanitizeStringsInClass(score);
foreach (User user in this.database.Users) SanitizationHelper.SanitizeStringsInClass(user);
foreach (Photo photo in this.database.Photos) SanitizationHelper.SanitizeStringsInClass(photo);
foreach (GriefReport report in this.database.Reports) SanitizationHelper.SanitizeStringsInClass(report);
await this.database.SaveChangesAsync();
}
}

View file

@ -1,4 +1,5 @@
@page "/slot/{id:int}" @page "/slot/{id:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions @using LBPUnion.ProjectLighthouse.Helpers.Extensions
@model LBPUnion.ProjectLighthouse.Pages.SlotPage @model LBPUnion.ProjectLighthouse.Pages.SlotPage
@ -31,7 +32,7 @@
<div class="eight wide column"> <div class="eight wide column">
<div class="ui blue segment"> <div class="ui blue segment">
<h2>Description</h2> <h2>Description</h2>
<p>@(string.IsNullOrEmpty(Model.Slot.Description) ? "This level has no description." : Model.Slot.Description)</p> <p>@HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model.Slot.Description) ? "This level has no description." : Model.Slot.Description)</p>
</div> </div>
</div> </div>
<div class="eight wide column"> <div class="eight wide column">

View file

@ -1,4 +1,5 @@
@page "/user/{userId:int}" @page "/user/{userId:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions @using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Pages.UserPage @model LBPUnion.ProjectLighthouse.Pages.UserPage
@ -82,7 +83,7 @@
} }
else else
{ {
<p>@Model.ProfileUser.Biography</p> <p>@HttpUtility.HtmlDecode(Model.ProfileUser.Biography)</p>
} }
</div> </div>
</div> </div>
@ -120,14 +121,14 @@
@if (!Model.ProfileUser.Banned) @if (!Model.ProfileUser.Banned)
{ {
<a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/ban"> <div>
<i class="ban icon"></i> <a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/ban">
<span>Ban User</span> <i class="ban icon"></i>
</a> <span>Ban User</span>
</a>
<br><br> </div>
<div class="ui fitted hidden divider"></div>
} }
@await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.ProfileUser) @await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.ProfileUser)
</div> </div>
} }

View file

@ -131,12 +131,12 @@ public class Slot
[XmlIgnore] [XmlIgnore]
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
public int Hearts => database.HeartedLevels.Count(s => s.SlotId == this.SlotId); public int Hearts => this.database.HeartedLevels.Count(s => s.SlotId == this.SlotId);
[XmlIgnore] [XmlIgnore]
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
public int Comments => database.Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId); public int Comments => this.database.Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId);
[XmlIgnore] [XmlIgnore]
[NotMapped] [NotMapped]
@ -201,19 +201,19 @@ public class Slot
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
[XmlElement("thumbsup")] [XmlElement("thumbsup")]
public int Thumbsup => database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1); public int Thumbsup => this.database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1);
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
[XmlElement("thumbsdown")] [XmlElement("thumbsdown")]
public int Thumbsdown => database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1); public int Thumbsdown => this.database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1);
[NotMapped] [NotMapped]
[JsonPropertyName("averageRating")] [JsonPropertyName("averageRating")]
[XmlElement("averageRating")] [XmlElement("averageRating")]
public double RatingLBP1 { public double RatingLBP1 {
get { get {
IQueryable<RatedLevel> ratedLevels = database.RatedLevels.Where(r => r.SlotId == this.SlotId && r.RatingLBP1 > 0); IQueryable<RatedLevel> ratedLevels = this.database.RatedLevels.Where(r => r.SlotId == this.SlotId && r.RatingLBP1 > 0);
if (!ratedLevels.Any()) return 3.0; if (!ratedLevels.Any()) return 3.0;
return Enumerable.Average(ratedLevels, r => r.RatingLBP1); return Enumerable.Average(ratedLevels, r => r.RatingLBP1);
@ -223,7 +223,7 @@ public class Slot
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
[XmlElement("reviewCount")] [XmlElement("reviewCount")]
public int ReviewCount => database.Reviews.Count(r => r.SlotId == this.SlotId); public int ReviewCount => this.database.Reviews.Count(r => r.SlotId == this.SlotId);
[XmlElement("leveltype")] [XmlElement("leveltype")]
public string LevelType { get; set; } = ""; public string LevelType { get; set; } = "";

View file

@ -1,8 +1,6 @@
#nullable enable #nullable enable
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.IO;
using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
@ -21,19 +19,19 @@ public class Review
public int ReviewerId { get; set; } public int ReviewerId { get; set; }
[ForeignKey(nameof(ReviewerId))] [ForeignKey(nameof(ReviewerId))]
public User Reviewer { get; set; } public User? Reviewer { get; set; }
[XmlElement("slot_id")] [XmlElement("slot_id")]
public int SlotId { get; set; } public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))] [ForeignKey(nameof(SlotId))]
public Slot Slot { get; set; } public Slot? Slot { get; set; }
[XmlElement("timestamp")] [XmlElement("timestamp")]
public long Timestamp { get; set; } public long Timestamp { get; set; }
[XmlElement("labels")] [XmlElement("labels")]
public string LabelCollection { get; set; } public string LabelCollection { get; set; } = "";
[NotMapped] [NotMapped]
[XmlIgnore] [XmlIgnore]
@ -49,7 +47,7 @@ public class Review
public DeletedBy DeletedBy { get; set; } public DeletedBy DeletedBy { get; set; }
[XmlElement("text")] [XmlElement("text")]
public string Text { get; set; } public string Text { get; set; } = "";
[XmlElement("thumb")] [XmlElement("thumb")]
public int Thumb { get; set; } public int Thumb { get; set; }
@ -66,27 +64,26 @@ public class Review
public string Serialize(string elementOverride, RatedLevel? yourLevelRating = null, RatedReview? yourRatingStats = null) public string Serialize(string elementOverride, RatedLevel? yourLevelRating = null, RatedReview? yourRatingStats = null)
{ {
string deletedBy = this.DeletedBy switch
{
DeletedBy.None => "none",
DeletedBy.Moderator => "moderator",
DeletedBy.LevelAuthor => "level_author",
_ => "none",
};
XmlWriterSettings settings = new(); string reviewData = LbpSerializer.TaggedStringElement("slot_id", this.SlotId, "type", this.Slot?.Type) +
settings.OmitXmlDeclaration = true; LbpSerializer.StringElement("reviewer", this.Reviewer?.Username) +
XmlSerializer serializer = new(typeof(DeletedBy));
StringWriter stringWriter = new();
using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings)) serializer.Serialize(xmlWriter, this.DeletedBy);
string deletedBy = stringWriter.ToString();
string reviewData = LbpSerializer.TaggedStringElement("slot_id", this.SlotId, "type", this.Slot.Type) +
LbpSerializer.StringElement("reviewer", this.Reviewer.Username) +
LbpSerializer.StringElement("thumb", this.Thumb) + LbpSerializer.StringElement("thumb", this.Thumb) +
LbpSerializer.StringElement("timestamp", this.Timestamp) + LbpSerializer.StringElement("timestamp", this.Timestamp) +
LbpSerializer.StringElement("labels", this.LabelCollection) + LbpSerializer.StringElement("labels", this.LabelCollection) +
LbpSerializer.StringElement("deleted", this.Deleted) + LbpSerializer.StringElement("deleted", this.Deleted) +
deletedBy + LbpSerializer.StringElement("deleted_by", deletedBy) +
LbpSerializer.StringElement("text", this.Text) + LbpSerializer.StringElement("text", this.Text) +
LbpSerializer.StringElement("thumbsup", this.ThumbsUp) + LbpSerializer.StringElement("thumbsup", this.ThumbsUp) +
LbpSerializer.StringElement("thumbsdown", this.ThumbsDown) + LbpSerializer.StringElement("thumbsdown", this.ThumbsDown) +
LbpSerializer.StringElement("yourthumb", yourRatingStats?.Thumb == null ? 0 : yourRatingStats?.Thumb); LbpSerializer.StringElement("yourthumb", yourRatingStats?.Thumb ?? 0);
return LbpSerializer.TaggedStringElement(elementOverride, reviewData, "id", this.SlotId + "." + this.Reviewer.Username); return LbpSerializer.TaggedStringElement(elementOverride, reviewData, "id", this.SlotId + "." + this.Reviewer?.Username);
} }
} }

View file

@ -7,23 +7,23 @@ namespace LBPUnion.ProjectLighthouse.Types;
public class UserUpdate public class UserUpdate
{ {
[XmlElement("location")] [XmlElement("location")]
public Location? Location; public Location? Location { get; set; }
[XmlElement("biography")] [XmlElement("biography")]
public string? Biography; public string? Biography { get; set; }
[XmlElement("icon")] [XmlElement("icon")]
public string? IconHash; public string? IconHash { get; set; }
[XmlElement("planets")] [XmlElement("planets")]
public string? PlanetHash; public string? PlanetHash { get; set; }
[XmlElement("yay2")] [XmlElement("yay2")]
public string? YayHash; public string? YayHash { get; set; }
[XmlElement("meh2")] [XmlElement("meh2")]
public string? MehHash; public string? MehHash { get; set; }
[XmlElement("boo2")] [XmlElement("boo2")]
public string? BooHash; public string? BooHash { get; set; }
} }