Skip to content

Commit ff54771

Browse files
authored
Trim various user inputs if needed (#969)
Makes various string inputs entered by users through both the API and the game get trimmed if they're too long. Also makes trimming behaviour across game and API endpoints more consistent (e.g. too long descriptions in game user updates will now just be trimmed, instead of the entire request being rejected), and invalid labels in API review submissions will just be removed for consistency, instead of the whole review being rejected.
2 parents 4949422 + 048c3b5 commit ff54771

File tree

18 files changed

+196
-46
lines changed

18 files changed

+196
-46
lines changed

Refresh.Interfaces.APIv3/Endpoints/ApiTypes/Errors/ApiValidationError.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ public class ApiValidationError : ApiError
3737

3838
public const string DontReviewLevelBeforePlayingWhen = "You may not review levels you haven't played yet";
3939
public static readonly ApiValidationError DontReviewLevelBeforePlaying = new(DontReviewLevelBeforePlayingWhen);
40-
41-
public const string ReviewHasInvalidLabelsWhen = "Your review contained invalid labels";
42-
public static readonly ApiValidationError ReviewHasInvalidLabels = new(ReviewHasInvalidLabelsWhen);
4340

4441
public const string HashInvalidErrorWhen = "The hash is invalid (should be SHA1 hash)";
4542
public static readonly ApiValidationError HashInvalidError = new(HashInvalidErrorWhen);

Refresh.Interfaces.APIv3/Endpoints/CommentApiEndpoints.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Bunkum.Core;
33
using Bunkum.Core.Endpoints;
44
using Bunkum.Protocols.Http;
5+
using Refresh.Common.Constants;
56
using Refresh.Core.Types.Data;
67
using Refresh.Database;
78
using Refresh.Database.Models.Comments;
@@ -43,6 +44,10 @@ public ApiResponse<ApiProfileCommentResponse> PostCommentOnProfile(RequestContex
4344
GameUser? profile = dataContext.Database.GetUserByUuid(uuid);
4445
if (profile == null) return ApiNotFoundError.UserMissingError;
4546

47+
// Trim content
48+
if (body.Content.Length > UgcLimits.CommentLimit)
49+
body.Content = body.Content[..UgcLimits.CommentLimit];
50+
4651
GameProfileComment comment = dataContext.Database.PostCommentToProfile(profile, user, body.Content);
4752
return ApiProfileCommentResponse.FromOld(comment, dataContext);
4853
}
@@ -118,6 +123,10 @@ public ApiResponse<ApiLevelCommentResponse> PostCommentOnLevel(RequestContext co
118123
GameLevel? level = dataContext.Database.GetLevelById(id);
119124
if (level == null) return ApiNotFoundError.LevelMissingError;
120125

126+
// Trim content
127+
if (body.Content.Length > UgcLimits.CommentLimit)
128+
body.Content = body.Content[..UgcLimits.CommentLimit];
129+
121130
GameLevelComment comment = dataContext.Database.PostCommentToLevel(level, user, body.Content);
122131
return ApiLevelCommentResponse.FromOld(comment, dataContext);
123132
}

Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Bunkum.Core.Endpoints;
44
using Bunkum.Core.Storage;
55
using Bunkum.Protocols.Http;
6+
using Refresh.Common.Constants;
67
using Refresh.Common.Verification;
78
using Refresh.Core.Authentication.Permission;
89
using Refresh.Core.Services;
@@ -63,6 +64,13 @@ public ApiResponse<ApiGameLevelResponse> EditLevelById(RequestContext context,
6364
!dataContext.GuidChecker.IsTextureGuid(level.GameVersion, long.Parse(body.IconHash)))
6465
return ApiValidationError.InvalidTextureGuidError;
6566

67+
// Trim title and description
68+
if (body.Title != null && body.Title.Length > UgcLimits.TitleLimit)
69+
body.Title = body.Title[..UgcLimits.TitleLimit];
70+
71+
if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit)
72+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
73+
6674
level = dataContext.Database.UpdateLevel(body, level, dataContext.User);
6775

6876
return ApiGameLevelResponse.FromOld(level, dataContext);

Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,28 +84,21 @@ public ApiResponse<ApiGameReviewResponse> GetReviewById(RequestContext context,
8484
}
8585

8686
/// <returns>
87-
/// The validated list of labels. If null, return BadRequest.
87+
/// The validated list of labels. Labels which are duplicates or invalid will be removed.
88+
/// Also, the list will be trimmed if there are too many labels.
8889
/// </returns>
89-
private List<Label>? ValidateLabels(List<Label> input)
90-
{
91-
// Duplicate labels aren't as bad as invalid ones, so just remove them.
92-
// Same with too many labels.
93-
List<Label> ret = input.Distinct().Take(UgcLimits.MaximumLabels).ToList();
94-
95-
foreach (Label label in ret)
96-
{
97-
if (!Enum.IsDefined(label)) return null;
98-
}
99-
100-
return ret;
101-
}
90+
private List<Label> ValidateLabels(List<Label> input)
91+
=> input
92+
.Distinct()
93+
.Where(l => Enum.IsDefined(l))
94+
.Take(UgcLimits.MaximumLabels)
95+
.ToList();
10296

10397
[ApiV3Endpoint("levels/id/{id}/reviews", HttpMethods.Post)]
10498
[DocSummary("Posts a review to the specified level. Updates the user's current review if they've already posted one.")]
10599
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)]
106100
[DocError(typeof(ApiValidationError), ApiValidationError.DontReviewOwnLevelWhen)]
107101
[DocError(typeof(ApiValidationError), ApiValidationError.DontReviewLevelBeforePlayingWhen)]
108-
[DocError(typeof(ApiValidationError), ApiValidationError.ReviewHasInvalidLabelsWhen)]
109102
[DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)]
110103
public ApiResponse<ApiGameReviewResponse> PostReviewToLevel(RequestContext context,
111104
GameDatabaseContext database, IDataStore dataStore, GameUser user,
@@ -126,9 +119,6 @@ public ApiResponse<ApiGameReviewResponse> PostReviewToLevel(RequestContext conte
126119
if (body.Labels != null)
127120
{
128121
body.Labels = this.ValidateLabels(body.Labels);
129-
130-
if (body.Labels == null)
131-
return ApiValidationError.ReviewHasInvalidLabels;
132122
}
133123

134124
if (body.LevelRating != null)
@@ -139,10 +129,9 @@ public ApiResponse<ApiGameReviewResponse> PostReviewToLevel(RequestContext conte
139129
database.RateLevel(level, user, body.LevelRating.Value);
140130
}
141131

142-
// TODO: Use the comment char limit constant once the other PR is merged.
143-
if (body.Content != null && body.Content.Length > 4096)
132+
if (body.Content != null && body.Content.Length > UgcLimits.CommentLimit)
144133
{
145-
body.Content = body.Content[..4096];
134+
body.Content = body.Content[..UgcLimits.CommentLimit];
146135
}
147136

148137
GameReview review = database.AddReviewToLevel(body, level, user);
@@ -153,7 +142,6 @@ public ApiResponse<ApiGameReviewResponse> PostReviewToLevel(RequestContext conte
153142
[DocSummary("Updates a review by ID.")]
154143
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ReviewMissingErrorWhen)]
155144
[DocError(typeof(ApiValidationError), ApiValidationError.NoReviewEditPermissionErrorWhen)]
156-
[DocError(typeof(ApiValidationError), ApiValidationError.ReviewHasInvalidLabelsWhen)]
157145
[DocError(typeof(ApiValidationError), ApiValidationError.RatingParseErrorWhen)]
158146
public ApiResponse<ApiGameReviewResponse> UpdateReviewById(RequestContext context,
159147
GameDatabaseContext database, IDataStore dataStore, GameUser user,
@@ -170,9 +158,6 @@ public ApiResponse<ApiGameReviewResponse> UpdateReviewById(RequestContext contex
170158
if (body.Labels != null)
171159
{
172160
body.Labels = this.ValidateLabels(body.Labels);
173-
174-
if (body.Labels == null)
175-
return ApiValidationError.ReviewHasInvalidLabels;
176161
}
177162

178163
if (body.LevelRating != null)
@@ -183,9 +168,9 @@ public ApiResponse<ApiGameReviewResponse> UpdateReviewById(RequestContext contex
183168
database.RateLevel(review.Level, user, body.LevelRating.Value);
184169
}
185170

186-
if (body.Content != null && body.Content.Length > 4096)
171+
if (body.Content != null && body.Content.Length > UgcLimits.CommentLimit)
187172
{
188-
body.Content = body.Content[..4096];
173+
body.Content = body.Content[..UgcLimits.CommentLimit];
189174
}
190175

191176
review = database.UpdateReview(body, review);

Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Bunkum.Core.Endpoints;
44
using Bunkum.Core.Storage;
55
using Bunkum.Protocols.Http;
6+
using Refresh.Common.Constants;
67
using Refresh.Core.Authentication.Permission;
78
using Refresh.Core.Configuration;
89
using Refresh.Core.Services;
@@ -72,6 +73,10 @@ public ApiResponse<ApiExtendedGameUserResponse> UpdateUser(RequestContext contex
7273

7374
if (body.EmailAddress != null && !smtpService.CheckEmailDomainValidity(body.EmailAddress))
7475
return ApiValidationError.EmailDoesNotActuallyExistError;
76+
77+
// Trim description
78+
if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit)
79+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
7580

7681
database.UpdateUserData(user, body);
7782
return ApiExtendedGameUserResponse.FromOld(user, dataContext);

Refresh.Interfaces.Game/Endpoints/ChallengeEndpoints.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Bunkum.Core.Responses;
55
using Bunkum.Listener.Protocol;
66
using Bunkum.Protocols.Http;
7+
using Refresh.Common.Constants;
78
using Refresh.Core.Authentication.Permission;
89
using Refresh.Core.Configuration;
910
using Refresh.Core.Services;
@@ -82,6 +83,10 @@ public Response UploadChallenge(RequestContext context, DataContext dataContext,
8283
if (body.Criteria.Count > 1)
8384
dataContext.Logger.LogWarning(BunkumCategory.UserContent, $"Challenge by {user.Username} on level ID {level.LevelId} has {body.Criteria.Count} criteria, only the first one will be saved");
8485

86+
// Trim name
87+
if (body.Name.Length > UgcLimits.TitleLimit)
88+
body.Name = body.Name[..UgcLimits.TitleLimit];
89+
8590
GameChallenge challenge = dataContext.Database.CreateChallenge(body, level, user);
8691

8792
// Return a SerializedChallenge which is not body, else the game will not send the first score

Refresh.Interfaces.Game/Endpoints/CommentEndpoints.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ public Response PostProfileComment(RequestContext context, GameDatabaseContext d
3131

3232
if (body.Content.Length > UgcLimits.CommentLimit)
3333
{
34-
database.AddErrorNotification("Failed to post comment", $"Your comment under {profile.Username}'s profile couldn't be posted because it was too long.", user);
35-
return BadRequest;
34+
body.Content = body.Content[..UgcLimits.CommentLimit];
3635
}
3736

3837
// TODO: include a check for if the user wants to receive these types of notifications
@@ -96,8 +95,7 @@ public Response PostLevelComment(RequestContext context, GameDatabaseContext dat
9695

9796
if (body.Content.Length > UgcLimits.CommentLimit)
9897
{
99-
database.AddErrorNotification("Failed to post comment", $"Your comment under the level '{level.Title}' couldn't be posted because it was too long.", user);
100-
return BadRequest;
98+
body.Content = body.Content[..UgcLimits.CommentLimit];
10199
}
102100

103101
if (level.Publisher != null && !level.Publisher.Equals(user))

Refresh.Interfaces.Game/Endpoints/Playlists/Lbp1PlaylistEndpoints.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Bunkum.Core.Responses;
44
using Bunkum.Listener.Protocol;
55
using Bunkum.Protocols.Http;
6+
using Refresh.Common.Constants;
67
using Refresh.Core.Authentication.Permission;
78
using Refresh.Core.Configuration;
89
using Refresh.Core.Types.Data;
@@ -49,6 +50,13 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext,
4950
return BadRequest;
5051
}
5152

53+
// Trim name and description
54+
if (body.Name.Length > UgcLimits.TitleLimit)
55+
body.Name = body.Name[..UgcLimits.TitleLimit];
56+
57+
if (body.Description.Length > UgcLimits.DescriptionLimit)
58+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
59+
5260
// Create the playlist, marking it as the root playlist if the user does not have one set already
5361
GamePlaylist playlist = dataContext.Database.CreatePlaylist(user, body, rootPlaylist == null);
5462

@@ -155,6 +163,13 @@ public Response UpdatePlaylistMetadata(RequestContext context, GameServerConfig
155163
if (playlist.PublisherId != user.UserId)
156164
return Unauthorized;
157165

166+
// Trim name and description
167+
if (body.Name.Length > UgcLimits.TitleLimit)
168+
body.Name = body.Name[..UgcLimits.TitleLimit];
169+
170+
if (body.Description.Length > UgcLimits.DescriptionLimit)
171+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
172+
158173
database.UpdatePlaylist(playlist, body);
159174
return OK;
160175
}

Refresh.Interfaces.Game/Endpoints/Playlists/Lbp3PlaylistEndpoints.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Bunkum.Core.Responses;
44
using Bunkum.Listener.Protocol;
55
using Bunkum.Protocols.Http;
6+
using Refresh.Common.Constants;
67
using Refresh.Core.Authentication.Permission;
78
using Refresh.Core.Configuration;
89
using Refresh.Core.Types.Data;
@@ -30,6 +31,13 @@ public Response CreatePlaylist(RequestContext context, GameServerConfig config,
3031
// if the player has no root playlist yet, create a new one first
3132
rootPlaylist ??= dataContext.Database.CreateRootPlaylist(user);
3233

34+
// Trim name and description
35+
if (body.Name != null && body.Name.Length > UgcLimits.TitleLimit)
36+
body.Name = body.Name[..UgcLimits.TitleLimit];
37+
38+
if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit)
39+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
40+
3341
// create the actual playlist and add it to the root playlist
3442
GamePlaylist playlist = dataContext.Database.CreatePlaylist(user, body);
3543
dataContext.Database.AddPlaylistToPlaylist(playlist, rootPlaylist!);
@@ -53,6 +61,13 @@ public Response UpdatePlaylist(RequestContext context, GameServerConfig config,
5361
if (playlist.PublisherId != user.UserId)
5462
return Unauthorized;
5563

64+
// Trim name and description
65+
if (body.Name != null && body.Name.Length > UgcLimits.TitleLimit)
66+
body.Name = body.Name[..UgcLimits.TitleLimit];
67+
68+
if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit)
69+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
70+
5671
dataContext.Database.UpdatePlaylist(playlist, body);
5772

5873
// get playlist from database a second time to respond with it in its updated state

Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Bunkum.Core.Responses;
55
using Bunkum.Listener.Protocol;
66
using Bunkum.Protocols.Http;
7+
using Refresh.Common.Constants;
78
using Refresh.Common.Time;
89
using Refresh.Core.Authentication.Permission;
910
using Refresh.Core.Configuration;
@@ -120,6 +121,10 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas
120121
}));
121122
}
122123

124+
// Trim description
125+
if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit)
126+
body.Description = body.Description[..UgcLimits.DescriptionLimit];
127+
123128
GriefReport griefReport = new()
124129
{
125130
Reporter = user,

0 commit comments

Comments
 (0)