diff --git a/Refresh.Common/Extensions/StringExtensions.cs b/Refresh.Common/Extensions/StringExtensions.cs new file mode 100644 index 00000000..0e93ed44 --- /dev/null +++ b/Refresh.Common/Extensions/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace Refresh.Common.Extensions; + +public static class StringExtensions +{ + public static bool IsBlankHash(this string? hash) + { + return string.IsNullOrWhiteSpace(hash) || hash == "0"; + } +} \ No newline at end of file diff --git a/Refresh.Database/GameDatabaseContext.Assets.cs b/Refresh.Database/GameDatabaseContext.Assets.cs index 2e548c3c..a208e34b 100644 --- a/Refresh.Database/GameDatabaseContext.Assets.cs +++ b/Refresh.Database/GameDatabaseContext.Assets.cs @@ -10,7 +10,7 @@ public partial class GameDatabaseContext // Assets public GameAsset? GetAssetFromHash(string hash) { - if (hash == "0" || hash.StartsWith('g')) return null; + if (hash.IsBlankHash() || hash.StartsWith('g')) return null; return this.GameAssetsIncluded .FirstOrDefault(a => a.AssetHash == hash); diff --git a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs index f13fe7a2..fd930258 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs @@ -62,14 +62,43 @@ public ApiResponse UpdateUser(RequestContext contex GameUser user, ApiUpdateUserRequest body, IDataStore dataStore, DataContext dataContext, IntegrationConfig integrationConfig, SmtpService smtpService) { - if (body.IconHash != null && database.GetAssetFromHash(body.IconHash) == null) - return ApiNotFoundError.Instance; - - if (body.VitaIconHash != null && database.GetAssetFromHash(body.VitaIconHash) == null) - return ApiNotFoundError.Instance; - - if (body.BetaIconHash != null && database.GetAssetFromHash(body.BetaIconHash) == null) - return ApiNotFoundError.Instance; + // If any icon is requested to be reset, force its hash to be a specific value, + // to not allow uncontrolled values which would still count as blank/empty hash (e.g. unlimited whitespaces) + if (body.IconHash != null) + { + if (body.IconHash.IsBlankHash()) + { + body.IconHash = "0"; + } + else if (database.GetAssetFromHash(body.IconHash) == null) + { + return ApiNotFoundError.Instance; + } + } + + if (body.VitaIconHash != null) + { + if (body.VitaIconHash.IsBlankHash()) + { + body.VitaIconHash = "0"; + } + else if (database.GetAssetFromHash(body.VitaIconHash) == null) + { + return ApiNotFoundError.Instance; + } + } + + if (body.BetaIconHash != null) + { + if (body.BetaIconHash.IsBlankHash()) + { + body.BetaIconHash = "0"; + } + else if (database.GetAssetFromHash(body.BetaIconHash) == null) + { + return ApiNotFoundError.Instance; + } + } if (body.EmailAddress != null && !smtpService.CheckEmailDomainValidity(body.EmailAddress)) return ApiValidationError.EmailDoesNotActuallyExistError; diff --git a/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs index a679dd19..89af490f 100644 --- a/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs @@ -115,14 +115,17 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont return null; } } - else + else if (data.IconHash.IsBlankHash()) + { + // Force hash to be a specific value if the icon is supposed to be reset/default to a PSN avatar, + // to not allow uncontrolled values which would still count as blank/empty hash (e.g. unlimited whitespaces) + data.IconHash = "0"; + } + else if (!dataContext.DataStore.ExistsInStore(data.IconHash)) { //If the asset does not exist on the server, block the request - if (!dataContext.DataStore.ExistsInStore(data.IconHash)) - { - dataContext.Database.AddErrorNotification("Profile update failed", "Your avatar failed to update because the asset was missing on the server.", user); - return null; - } + dataContext.Database.AddErrorNotification("Profile update failed", "Your avatar failed to update because the asset was missing on the server.", user); + return null; } } diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index 1b4ee33a..7211b34b 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -163,6 +163,41 @@ public void CanPatchOwnUser() Assert.That(user.Description, Is.EqualTo(description)); } + [Test] + [TestCase("")] + [TestCase("0")] + public void CanResetOwnIcon(string newIcon) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, user); + + // Prepare by setting icon to something + string fakeIcon = "mmmmm"; + context.Database.UpdateUserData(user, new ApiUpdateUserRequest() + { + IconHash = fakeIcon + }); + GameUser? userPrepared = context.Database.GetUserByObjectId(user.UserId); + Assert.That(userPrepared, Is.Not.Null); + Assert.That(userPrepared!.IconHash, Is.EqualTo(fakeIcon)); + + // Now try resetting + ApiUpdateUserRequest request = new() + { + IconHash = newIcon + }; + ApiResponse? response = client.PatchData("/api/v3/users/me", request); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Data!.IconHash, Is.EqualTo("0")); + + context.Database.Refresh(); + + GameUser? userUpdated = context.Database.GetUserByObjectId(user.UserId); + Assert.That(userUpdated, Is.Not.Null); + Assert.That(userUpdated!.IconHash, Is.EqualTo("0")); + } + [Test] public void UpdateShowModdedPlanets() { diff --git a/RefreshTests.GameServer/Tests/Users/UserActionTests.cs b/RefreshTests.GameServer/Tests/Users/UserActionTests.cs index 147788ab..b5b4ba61 100644 --- a/RefreshTests.GameServer/Tests/Users/UserActionTests.cs +++ b/RefreshTests.GameServer/Tests/Users/UserActionTests.cs @@ -1,6 +1,7 @@ using Refresh.Common.Constants; using Refresh.Database.Models.Authentication; using Refresh.Database.Models.Users; +using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request; using Refresh.Interfaces.Game.Types.UserData; using RefreshTests.GameServer.Extensions; @@ -46,4 +47,38 @@ public void UserDescriptionGetsTrimmed() Assert.That(updated, Is.Not.Null); Assert.That(updated!.Description.Length, Is.EqualTo(UgcLimits.DescriptionLimit)); } + + [Test] + [TestCase("")] + [TestCase("0")] + public void CanResetOwnIcon(string newIcon) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); + + // Prepare by setting icon to something + string fakeIcon = "mmmmm"; + context.Database.UpdateUserData(user, new ApiUpdateUserRequest() + { + IconHash = fakeIcon + }); + GameUser? userPrepared = context.Database.GetUserByObjectId(user.UserId); + Assert.That(userPrepared, Is.Not.Null); + Assert.That(userPrepared!.IconHash, Is.EqualTo(fakeIcon)); + + // Now try resetting + SerializedUpdateDataPlanets request = new() + { + IconHash = newIcon + }; + HttpResponseMessage response = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result; + Assert.That(response, Is.Not.Null); + + context.Database.Refresh(); + + GameUser? userUpdated = context.Database.GetUserByObjectId(user.UserId); + Assert.That(userUpdated, Is.Not.Null); + Assert.That(userUpdated!.IconHash, Is.EqualTo("0")); + } } \ No newline at end of file