feat(backend): validate custom preferences on save
This commit is contained in:
parent
71b59dbb00
commit
8b1d5b2c1b
6 changed files with 560 additions and 477 deletions
|
@ -27,7 +27,7 @@ public class MetaController : ApiControllerBase
|
|||
new Limits(
|
||||
MemberCount: MembersController.MaxMemberCount,
|
||||
BioLength: ValidationUtils.MaxBioLength,
|
||||
CustomPreferences: UsersController.MaxCustomPreferences
|
||||
CustomPreferences: ValidationUtils.MaxCustomPreferences
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -197,11 +197,11 @@ public class UsersController(
|
|||
[Authorize("user.update")]
|
||||
[ProducesResponseType<Dictionary<Snowflake, User.CustomPreference>>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> UpdateCustomPreferencesAsync(
|
||||
[FromBody] List<CustomPreferencesUpdateRequest> req,
|
||||
[FromBody] List<CustomPreferenceUpdate> 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<CustomPreferencesUpdateRequest> 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; }
|
||||
|
|
287
Foxnouns.Backend/Utils/ValidationUtils.Fields.cs
Normal file
287
Foxnouns.Backend/Utils/ValidationUtils.Fields.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<Field>? fields,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> 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<Snowflake, User.CustomPreference> 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<Snowflake, User.CustomPreference> 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;
|
||||
}
|
||||
}
|
72
Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs
Normal file
72
Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<UsersController.CustomPreferenceUpdate> 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;
|
||||
}
|
||||
}
|
197
Foxnouns.Backend/Utils/ValidationUtils.Strings.cs
Normal file
197
Foxnouns.Backend/Utils/ValidationUtils.Strings.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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();
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
|
||||
namespace Foxnouns.Backend.Utils;
|
||||
|
||||
/// <summary>
|
||||
|
@ -9,76 +5,6 @@ namespace Foxnouns.Backend.Utils;
|
|||
/// </summary>
|
||||
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<Field>? fields,
|
||||
IReadOnlyDictionary<Snowflake, User.CustomPreference> 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<Snowflake, User.CustomPreference> 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<Snowflake, User.CustomPreference> 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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue