From 8b1d5b2c1b6a9afface067ada3b8d9b844b7ac0e Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 28 Nov 2024 17:28:52 +0100 Subject: [PATCH] feat(backend): validate custom preferences on save --- .../Controllers/MetaController.cs | 2 +- .../Controllers/UsersController.cs | 34 +- .../Utils/ValidationUtils.Fields.cs | 287 +++++++++++ .../Utils/ValidationUtils.Preferences.cs | 72 +++ .../Utils/ValidationUtils.Strings.cs | 197 ++++++++ Foxnouns.Backend/Utils/ValidationUtils.cs | 445 ------------------ 6 files changed, 560 insertions(+), 477 deletions(-) create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.Fields.cs create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.Strings.cs diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 411e2e8..d699fc2 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -27,7 +27,7 @@ public class MetaController : ApiControllerBase new Limits( MemberCount: MembersController.MaxMemberCount, BioLength: ValidationUtils.MaxBioLength, - CustomPreferences: UsersController.MaxCustomPreferences + CustomPreferences: ValidationUtils.MaxCustomPreferences ) ) ); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 2693bef..aa8d02d 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -197,11 +197,11 @@ public class UsersController( [Authorize("user.update")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task UpdateCustomPreferencesAsync( - [FromBody] List req, + [FromBody] List req, CancellationToken ct = default ) { - ValidationUtils.Validate(ValidateCustomPreferences(req)); + ValidationUtils.Validate(ValidationUtils.ValidateCustomPreferences(req)); var user = await db.ResolveUserAsync(CurrentUser!.Id, ct); var preferences = user @@ -241,7 +241,7 @@ public class UsersController( } [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - public class CustomPreferencesUpdateRequest + public class CustomPreferenceUpdate { public Snowflake? Id { get; init; } public required string Icon { get; set; } @@ -251,34 +251,6 @@ public class UsersController( public bool Favourite { get; set; } } - public const int MaxCustomPreferences = 25; - - private static List<(string, ValidationError?)> ValidateCustomPreferences( - List preferences - ) - { - var errors = new List<(string, ValidationError?)>(); - - if (preferences.Count > MaxCustomPreferences) - errors.Add( - ( - "custom_preferences", - ValidationError.LengthError( - "Too many custom preferences", - 0, - MaxCustomPreferences, - preferences.Count - ) - ) - ); - if (preferences.Count > 50) - return errors; - - // TODO: validate individual preferences - - return errors; - } - public class UpdateUserRequest : PatchRequest { public string? Username { get; init; } diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs new file mode 100644 index 0000000..1ed083c --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs @@ -0,0 +1,287 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; + +namespace Foxnouns.Backend.Utils; + +public static partial class ValidationUtils +{ + private static readonly string[] DefaultStatusOptions = + [ + "favourite", + "okay", + "jokingly", + "friends_only", + "avoid", + ]; + + public static IEnumerable<(string, ValidationError?)> ValidateFields( + List? fields, + IReadOnlyDictionary customPreferences + ) + { + if (fields == null) + return []; + + var errors = new List<(string, ValidationError?)>(); + if (fields.Count > 25) + errors.Add( + ( + "fields", + ValidationError.LengthError( + "Too many fields", + 0, + Limits.FieldLimit, + fields.Count + ) + ) + ); + // No overwhelming this function, thank you + if (fields.Count > 100) + return errors; + + foreach (var (field, index) in fields.Select((field, index) => (field, index))) + { + switch (field.Name.Length) + { + case > Limits.FieldNameLimit: + errors.Add( + ( + $"fields.{index}.name", + ValidationError.LengthError( + "Field name is too long", + 1, + Limits.FieldNameLimit, + field.Name.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"fields.{index}.name", + ValidationError.LengthError( + "Field name is too short", + 1, + Limits.FieldNameLimit, + field.Name.Length + ) + ) + ); + break; + } + + errors = errors + .Concat( + ValidateFieldEntries( + field.Entries, + customPreferences, + $"fields.{index}.entries" + ) + ) + .ToList(); + } + + return errors; + } + + public static IEnumerable<(string, ValidationError?)> ValidateFieldEntries( + FieldEntry[]? entries, + IReadOnlyDictionary customPreferences, + string errorPrefix = "fields" + ) + { + if (entries == null || entries.Length == 0) + return []; + var errors = new List<(string, ValidationError?)>(); + + if (entries.Length > Limits.FieldEntriesLimit) + errors.Add( + ( + errorPrefix, + ValidationError.LengthError( + "Field has too many entries", + 0, + Limits.FieldEntriesLimit, + entries.Length + ) + ) + ); + + // Same as above, no overwhelming this function with a ridiculous amount of entries + if (entries.Length > Limits.FieldEntriesLimit + 50) + return errors; + + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) + { + switch (entry.Value.Length) + { + case > Limits.FieldEntryTextLimit: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Field value is too long", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Field value is too short", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + } + + var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; + + if ( + !DefaultStatusOptions.Contains(entry.Status) + && !customPreferenceIds.Contains(entry.Status) + ) + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.status", + ValidationError.GenericValidationError("Invalid status", entry.Status) + ) + ); + } + + return errors; + } + + public static IEnumerable<(string, ValidationError?)> ValidatePronouns( + Pronoun[]? entries, + IReadOnlyDictionary customPreferences, + string errorPrefix = "pronouns" + ) + { + if (entries == null || entries.Length == 0) + return []; + var errors = new List<(string, ValidationError?)>(); + + if (entries.Length > Limits.FieldEntriesLimit) + errors.Add( + ( + errorPrefix, + ValidationError.LengthError( + "Too many pronouns", + 0, + Limits.FieldEntriesLimit, + entries.Length + ) + ) + ); + + // Same as above, no overwhelming this function with a ridiculous amount of entries + if (entries.Length > Limits.FieldEntriesLimit + 50) + return errors; + + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) + { + switch (entry.Value.Length) + { + case > Limits.FieldEntryTextLimit: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Pronoun value is too long", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Pronoun value is too short", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + } + + if (entry.DisplayText != null) + { + switch (entry.DisplayText.Length) + { + case > Limits.FieldEntryTextLimit: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.display_text", + ValidationError.LengthError( + "Pronoun display text is too long", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.display_text", + ValidationError.LengthError( + "Pronoun display text is too short", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + } + } + + var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; + + if ( + !DefaultStatusOptions.Contains(entry.Status) + && !customPreferenceIds.Contains(entry.Status) + ) + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.status", + ValidationError.GenericValidationError("Invalid status", entry.Status) + ) + ); + } + + return errors; + } +} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs new file mode 100644 index 0000000..379a552 --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs @@ -0,0 +1,72 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using Foxnouns.Backend.Controllers; + +namespace Foxnouns.Backend.Utils; + +public static partial class ValidationUtils +{ + public const int MaxCustomPreferences = 25; + public const int MaxPreferenceTooltipLength = 128; + + public static List<(string, ValidationError?)> ValidateCustomPreferences( + List preferences + ) + { + var errors = new List<(string, ValidationError?)>(); + + if (preferences.Count > MaxCustomPreferences) + errors.Add( + ( + "custom_preferences", + ValidationError.LengthError( + "Too many custom preferences", + 0, + MaxCustomPreferences, + preferences.Count + ) + ) + ); + if (preferences.Count > 50) + return errors; + + foreach (var (p, i) in preferences.Select((p, i) => (p, i))) + { + if (!BootstrapIcons.IsValid(p.Icon)) + errors.Add( + ( + $"custom_preferences.{i}.icon", + ValidationError.DisallowedValueError("Invalid icon name", [], p.Icon) + ) + ); + + if (p.Tooltip.Length is 1 or > MaxPreferenceTooltipLength) + errors.Add( + ( + $"custom_preferences.{i}.tooltip", + ValidationError.LengthError( + "Tooltip is too short or too long", + 1, + MaxPreferenceTooltipLength, + p.Tooltip.Length + ) + ) + ); + } + + return errors; + } +} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs new file mode 100644 index 0000000..0193b7e --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs @@ -0,0 +1,197 @@ +// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Text.RegularExpressions; + +namespace Foxnouns.Backend.Utils; + +public static partial class ValidationUtils +{ + private static readonly string[] InvalidUsernames = + [ + "..", + "admin", + "administrator", + "mod", + "moderator", + "api", + "page", + "pronouns", + "settings", + "pronouns.cc", + "pronounscc", + ]; + + private static readonly string[] InvalidMemberNames = + [ + // these break routing outright + ".", + "..", + // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible + "edit", + ]; + + public static ValidationError? ValidateUsername(string username) + { + if (!UsernameRegex().IsMatch(username)) + return username.Length switch + { + < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), + > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), + _ => ValidationError.GenericValidationError( + "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", + username + ), + }; + + if ( + InvalidUsernames.Any(u => + string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase) + ) + ) + return ValidationError.GenericValidationError("Username is not allowed", username); + return null; + } + + public static ValidationError? ValidateMemberName(string memberName) + { + if (!MemberRegex().IsMatch(memberName)) + return memberName.Length switch + { + < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), + > 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), + _ => ValidationError.GenericValidationError( + "Member name cannot contain any of the following: " + + " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , " + + "and cannot be one or two periods", + memberName + ), + }; + + if ( + InvalidMemberNames.Any(u => + string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase) + ) + ) + return ValidationError.GenericValidationError("Name is not allowed", memberName); + return null; + } + + public static ValidationError? ValidateDisplayName(string? displayName) + { + return displayName?.Length switch + { + 0 => ValidationError.LengthError( + "Display name is too short", + 1, + 100, + displayName.Length + ), + > 100 => ValidationError.LengthError( + "Display name is too long", + 1, + 100, + displayName.Length + ), + _ => null, + }; + } + + private const int MaxLinks = 25; + private const int MaxLinkLength = 256; + + public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) + { + if (links == null) + return []; + if (links.Length > MaxLinks) + return + [ + ("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)), + ]; + + var errors = new List<(string, ValidationError?)>(); + foreach (var (link, idx) in links.Select((l, i) => (l, i))) + { + switch (link.Length) + { + case 0: + errors.Add( + ( + $"links.{idx}", + ValidationError.LengthError("Link cannot be empty", 1, 256, 0) + ) + ); + break; + case > MaxLinkLength: + errors.Add( + ( + $"links.{idx}", + ValidationError.LengthError( + "Link is too long", + 1, + MaxLinkLength, + link.Length + ) + ) + ); + break; + } + } + + return errors; + } + + public const int MaxBioLength = 1024; + public const int MaxAvatarLength = 1_500_000; + + public static ValidationError? ValidateBio(string? bio) + { + return bio?.Length switch + { + 0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length), + > MaxBioLength => ValidationError.LengthError( + "Bio is too long", + 1, + MaxBioLength, + bio.Length + ), + _ => null, + }; + } + + public static ValidationError? ValidateAvatar(string? avatar) + { + return avatar?.Length switch + { + 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), + > MaxAvatarLength => ValidationError.GenericValidationError( + "Avatar is too large", + null + ), + _ => null, + }; + } + + [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] + private static partial Regex UsernameRegex(); + + [GeneratedRegex( + """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", + RegexOptions.IgnoreCase, + "en-NL" + )] + private static partial Regex MemberRegex(); +} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 3374e3e..3e0acd5 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -1,7 +1,3 @@ -using System.Text.RegularExpressions; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; - namespace Foxnouns.Backend.Utils; /// @@ -9,76 +5,6 @@ namespace Foxnouns.Backend.Utils; /// public static partial class ValidationUtils { - private static readonly string[] InvalidUsernames = - [ - "..", - "admin", - "administrator", - "mod", - "moderator", - "api", - "page", - "pronouns", - "settings", - "pronouns.cc", - "pronounscc", - ]; - - private static readonly string[] InvalidMemberNames = - [ - // these break routing outright - ".", - "..", - // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible - "edit", - ]; - - public static ValidationError? ValidateUsername(string username) - { - if (!UsernameRegex().IsMatch(username)) - return username.Length switch - { - < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), - > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), - _ => ValidationError.GenericValidationError( - "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", - username - ), - }; - - if ( - InvalidUsernames.Any(u => - string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase) - ) - ) - return ValidationError.GenericValidationError("Username is not allowed", username); - return null; - } - - public static ValidationError? ValidateMemberName(string memberName) - { - if (!MemberRegex().IsMatch(memberName)) - return memberName.Length switch - { - < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), - > 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), - _ => ValidationError.GenericValidationError( - "Member name cannot contain any of the following: " - + " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , " - + "and cannot be one or two periods", - memberName - ), - }; - - if ( - InvalidMemberNames.Any(u => - string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase) - ) - ) - return ValidationError.GenericValidationError("Name is not allowed", memberName); - return null; - } - public static void Validate(IEnumerable<(string, ValidationError?)> errors) { errors = errors.Where(e => e.Item2 != null).ToList(); @@ -95,375 +21,4 @@ public static partial class ValidationUtils throw new ApiError.BadRequest("Error validating input", errorDict); } - - public static ValidationError? ValidateDisplayName(string? displayName) - { - return displayName?.Length switch - { - 0 => ValidationError.LengthError( - "Display name is too short", - 1, - 100, - displayName.Length - ), - > 100 => ValidationError.LengthError( - "Display name is too long", - 1, - 100, - displayName.Length - ), - _ => null, - }; - } - - private const int MaxLinks = 25; - private const int MaxLinkLength = 256; - - public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) - { - if (links == null) - return []; - if (links.Length > MaxLinks) - return - [ - ("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)), - ]; - - var errors = new List<(string, ValidationError?)>(); - foreach (var (link, idx) in links.Select((l, i) => (l, i))) - { - switch (link.Length) - { - case 0: - errors.Add( - ( - $"links.{idx}", - ValidationError.LengthError("Link cannot be empty", 1, 256, 0) - ) - ); - break; - case > MaxLinkLength: - errors.Add( - ( - $"links.{idx}", - ValidationError.LengthError( - "Link is too long", - 1, - MaxLinkLength, - link.Length - ) - ) - ); - break; - } - } - - return errors; - } - - public const int MaxBioLength = 1024; - public const int MaxAvatarLength = 1_500_000; - - public static ValidationError? ValidateBio(string? bio) - { - return bio?.Length switch - { - 0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length), - > MaxBioLength => ValidationError.LengthError( - "Bio is too long", - 1, - MaxBioLength, - bio.Length - ), - _ => null, - }; - } - - public static ValidationError? ValidateAvatar(string? avatar) - { - return avatar?.Length switch - { - 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), - > MaxAvatarLength => ValidationError.GenericValidationError( - "Avatar is too large", - null - ), - _ => null, - }; - } - - private static readonly string[] DefaultStatusOptions = - [ - "favourite", - "okay", - "jokingly", - "friends_only", - "avoid", - ]; - - public static IEnumerable<(string, ValidationError?)> ValidateFields( - List? fields, - IReadOnlyDictionary customPreferences - ) - { - if (fields == null) - return []; - - var errors = new List<(string, ValidationError?)>(); - if (fields.Count > 25) - errors.Add( - ( - "fields", - ValidationError.LengthError( - "Too many fields", - 0, - Limits.FieldLimit, - fields.Count - ) - ) - ); - // No overwhelming this function, thank you - if (fields.Count > 100) - return errors; - - foreach (var (field, index) in fields.Select((field, index) => (field, index))) - { - switch (field.Name.Length) - { - case > Limits.FieldNameLimit: - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too long", - 1, - Limits.FieldNameLimit, - field.Name.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too short", - 1, - Limits.FieldNameLimit, - field.Name.Length - ) - ) - ); - break; - } - - errors = errors - .Concat( - ValidateFieldEntries( - field.Entries, - customPreferences, - $"fields.{index}.entries" - ) - ) - .ToList(); - } - - return errors; - } - - public static IEnumerable<(string, ValidationError?)> ValidateFieldEntries( - FieldEntry[]? entries, - IReadOnlyDictionary customPreferences, - string errorPrefix = "fields" - ) - { - if (entries == null || entries.Length == 0) - return []; - var errors = new List<(string, ValidationError?)>(); - - if (entries.Length > Limits.FieldEntriesLimit) - errors.Add( - ( - errorPrefix, - ValidationError.LengthError( - "Field has too many entries", - 0, - Limits.FieldEntriesLimit, - entries.Length - ) - ) - ); - - // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > Limits.FieldEntriesLimit + 50) - return errors; - - foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) - { - switch (entry.Value.Length) - { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too short", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - } - - var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; - - if ( - !DefaultStatusOptions.Contains(entry.Status) - && !customPreferenceIds.Contains(entry.Status) - ) - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.status", - ValidationError.GenericValidationError("Invalid status", entry.Status) - ) - ); - } - - return errors; - } - - public static IEnumerable<(string, ValidationError?)> ValidatePronouns( - Pronoun[]? entries, - IReadOnlyDictionary customPreferences, - string errorPrefix = "pronouns" - ) - { - if (entries == null || entries.Length == 0) - return []; - var errors = new List<(string, ValidationError?)>(); - - if (entries.Length > Limits.FieldEntriesLimit) - errors.Add( - ( - errorPrefix, - ValidationError.LengthError( - "Too many pronouns", - 0, - Limits.FieldEntriesLimit, - entries.Length - ) - ) - ); - - // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > Limits.FieldEntriesLimit + 50) - return errors; - - foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) - { - switch (entry.Value.Length) - { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too short", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - } - - if (entry.DisplayText != null) - { - switch (entry.DisplayText.Length) - { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too short", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - } - } - - var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; - - if ( - !DefaultStatusOptions.Contains(entry.Status) - && !customPreferenceIds.Contains(entry.Status) - ) - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.status", - ValidationError.GenericValidationError("Invalid status", entry.Status) - ) - ); - } - - return errors; - } - - [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] - private static partial Regex UsernameRegex(); - - [GeneratedRegex( - """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", - RegexOptions.IgnoreCase, - "en-NL" - )] - private static partial Regex MemberRegex(); }