diff --git a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs new file mode 100644 index 000000000..ab0971904 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs @@ -0,0 +1,137 @@ +using System.Buffers; +using Server.Misc; +using Xunit; + +namespace Server.Tests; + +public class NameVerificationTests +{ + [Theory] + [InlineData("John")] + [InlineData("Mary Ann")] + [InlineData("Bob-Jones")] + [InlineData("O'Malley")] + public void ValidatePlayerName_ValidNames_ReturnsTrue(string name) + { + Assert.True(NameVerification.ValidatePlayerName(name)); + } + + [Theory] + [InlineData("Rex")] + [InlineData("MrWhiskers")] + [InlineData("Fido")] + public void ValidatePetName_ValidNames_ReturnsTrue(string name) + { + Assert.True(NameVerification.ValidatePetName(name)); + } + + [Theory] + [InlineData("Mrs-Smith")] + [InlineData("Mr. Whiskers")] + [InlineData("Dog123")] + public void ValidatePetName_InvalidNames_ReturnsFalse(string name) + { + Assert.False(NameVerification.ValidatePetName(name)); + } + + [Theory] + [InlineData("Blacksmith12")] + [InlineData("Baker")] + [InlineData("Innkeeper")] + public void ValidateVendorName_ValidNames_ReturnsTrue(string name) + { + Assert.True(NameVerification.ValidateVendorName(name)); + } + + [Theory] + [InlineData("Blacksmith 123")] + [InlineData("Baker-Smith")] + [InlineData("*Innkeeper*")] + public void ValidateVendorName_ValidNames_ReturnsFalse(string name) + { + Assert.False(NameVerification.ValidateVendorName(name)); + } + + [Fact] + public void Validate_MinimumLengthName_ReturnsTrue() + { + Assert.True(NameVerification.Validate("Ab", 2, 16, true, false)); + } + + [Fact] + public void Validate_MaximumLengthName_ReturnsTrue() + { + Assert.True(NameVerification.Validate("AbcdefghijklmnopqrsT", 1, 20, true, true)); + } + + [Fact] + public void Validate_MaxExceptions_ReturnsTrue() + { + var exceptions = SearchValues.Create(' ', '-', '.'); + Assert.True(NameVerification.Validate("A-B.C D", 2, 16, true, false, false, 3, exceptions)); + } + + [Fact] + public void Validate_BoundaryOfDisallowedWord_ReturnsTrueWhenNotActuallyDisallowed() + { + // "ass" is in disallowed, but "class" has proper boundaries + Assert.True(NameVerification.ValidatePlayerName("Class")); + } + + // Negative Tests + [Fact] + public void Validate_EmptyName_ReturnsFalse() + { + Assert.False(NameVerification.Validate("", 1, 20, true, true)); + } + + [Fact] + public void Validate_TooShortName_ReturnsFalse() + { + Assert.False(NameVerification.Validate("A", 2, 16, true, false)); + } + + [Fact] + public void Validate_TooLongName_ReturnsFalse() + { + Assert.False(NameVerification.Validate("AbcdefghijklmnopqrstuvwxyzABCDEF", 2, 16, true, false)); + } + + [Theory] + [InlineData("ass")] + [InlineData("GodDamn Fine")] + [InlineData("Fuck")] + public void Validate_DisallowedWords_ReturnsFalse(string name) + { + Assert.False(NameVerification.ValidatePlayerName(name)); + } + + [Theory] + [InlineData("GMJohn")] + [InlineData("LordBob")] + [InlineData("SeerMagic")] + public void Validate_DisallowedPrefixes_ReturnsFalse(string name) + { + Assert.False(NameVerification.ValidatePlayerName(name)); + } + + [Fact] + public void Validate_TooManyExceptions_ReturnsFalse() + { + var exceptions = SearchValues.Create(' ', '-', '.'); + Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 3, exceptions)); + } + + [Fact] + public void Validate_ExceptionAtStartWhenNotAllowed_ReturnsFalse() + { + var exceptions = SearchValues.Create(' ', '-', '.'); + Assert.False(NameVerification.Validate("-John", 2, 16, true, false, true, 1, exceptions)); + } + + [Fact] + public void Validate_DisallowedCharacters_ReturnsFalse() + { + Assert.False(NameVerification.Validate("John123", 2, 16, true, false)); + } +} diff --git a/Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs b/Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs new file mode 100644 index 000000000..012d649b8 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Misc/ProfanityProtectionTests.cs @@ -0,0 +1,53 @@ +using Server.Misc; +using Xunit; + +namespace Server.Tests; + +public class ProfanityProtectionTests +{ + [Theory] + [InlineData("Hello world")] + [InlineData("This is a normal conversation")] + [InlineData("I would like to trade with you")] + public void Speech_WithoutProfanity_PassesValidation(string speech) + { + Assert.False(ProfanityProtection.ContainsProfanity(speech)); + } + + [Fact] + public void Speech_Empty_PassesValidation() + { + Assert.False(ProfanityProtection.ContainsProfanity("")); + } + + [Theory] + [InlineData("I'm going to class tomorrow")] // contains "ass" but in "class" + [InlineData("This assignment is hard")] // contains "ass" but in "assignment" + [InlineData("That's a nice cocktail")] // contains "cock" but in "cocktail" + public void Speech_WithWordsThatLookLikeProfanity_PassesValidation(string speech) + { + Assert.False(ProfanityProtection.ContainsProfanity(speech)); + } + + [Theory] + [InlineData("This is ass")] + [InlineData("What the fuck")] + [InlineData("You're a bitch")] + public void Speech_WithProfanity_FailsValidation(string speech) + { + Assert.True(ProfanityProtection.ContainsProfanity(speech)); + } + + [Theory] + [InlineData("ass")] // standalone profanity + [InlineData("an ass joke")] // profanity with word boundaries + [InlineData("ass.")] // profanity followed by punctuation + public void ContainsDisallowedWord_DetectsProfanityWithBoundaries(string speech) + { + Assert.True(NameVerification.ContainsDisallowedWord( + speech, + ProfanityProtection.Disallowed, + ProfanityProtection.DisallowedSearchValues + )); + } +} diff --git a/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs b/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs index b743b9da7..87204e7fc 100644 --- a/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs +++ b/Projects/UOContent/Engines/Character Creation/CharacterCreation.cs @@ -418,7 +418,7 @@ public static partial class CharacterCreation { name = name.Trim(); - if (!NameVerification.Validate(name, 2, 16, true, false, true, 1, NameVerification.SpaceDashPeriodQuote)) + if (!NameVerification.ValidatePlayerName(name)) { name = "Generic Player"; } diff --git a/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs b/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs index ee4c36dce..9aa1b5152 100644 --- a/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs +++ b/Projects/UOContent/Gumps/Guilds/New Guild System/BaseGuildGump.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Server.Gumps; using Server.Misc; using Server.Mobiles; @@ -78,55 +79,20 @@ namespace Server.Guilds !(m.Deleted || g.Disbanded || m is not PlayerMobile || m.AccessLevel < AccessLevel.GameMaster && !g.IsMember(m)); - public static bool CheckProfanity(string s, int maxLength = 50) - { - // return NameVerification.Validate( s, 1, 50, true, true, false, int.MaxValue, ProfanityProtection.Exceptions, ProfanityProtection.Disallowed, ProfanityProtection.StartDisallowed ); //What am I doing wrong, this still allows chars like the <3 symbol... 3 AM. someone change this to use this - - // With testing on OSI, Guild stuff seems to follow a 'simpler' method of profanity protection - if (s.Length < 1 || s.Length > maxLength) - { - return false; - } - - var exceptions = ProfanityProtection.Exceptions; - - s = s.ToLower(); - - for (var i = 0; i < s.Length; ++i) - { - var c = s[i]; - - if (c is < 'a' or > 'z' && c is < '0' or > '9') - { - var except = false; - - for (var j = 0; !except && j < exceptions.Length; j++) - { - if (c == exceptions[j]) - { - except = true; - } - } - - if (!except) - { - return false; - } - } - } - - var disallowed = ProfanityProtection.Disallowed; - - for (var i = 0; i < disallowed.Length; i++) - { - if (s.IndexOfOrdinal(disallowed[i]) != -1) - { - return false; - } - } - - return true; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckProfanity(string s, int maxLength = 50) => + NameVerification.Validate( + s, + 1, + maxLength, + true, + true, + false, + 0, + ProfanityProtection.Exceptions, + ProfanityProtection.Disallowed, + ProfanityProtection.DisallowedSearchValues + ); public void AddHtmlText(int x, int y, int width, int height, TextDefinition text, bool back, bool scroll) { diff --git a/Projects/UOContent/Items/Deeds/NameChangeDeed.cs b/Projects/UOContent/Items/Deeds/NameChangeDeed.cs index b21320604..0b2d2aa31 100644 --- a/Projects/UOContent/Items/Deeds/NameChangeDeed.cs +++ b/Projects/UOContent/Items/Deeds/NameChangeDeed.cs @@ -1,3 +1,4 @@ +using System; using ModernUO.Serialization; using Server.Gumps; using Server.Misc; @@ -79,15 +80,15 @@ public class NameChangeDeedGump : Gump var m = sender.Mobile; - var newName = info.GetTextEntry(0)?.Trim(); + var newName = info.GetTextEntry(0).AsSpan().Trim(); - if (!NameVerification.Validate(newName, 2, 16, true, false, true, 1, NameVerification.SpaceDashPeriodQuote)) + if (!NameVerification.ValidatePlayerName(newName)) { m.SendMessage("That name is unacceptable."); return; } - m.RawName = newName; + m.RawName = newName.ToString(); m.SendMessage("Your name has been changed!"); m.SendMessage($"You are now known as {newName}"); m_Sender.Delete(); diff --git a/Projects/UOContent/Misc/NameVerification.cs b/Projects/UOContent/Misc/NameVerification.cs index e64c41bf9..ae934b261 100644 --- a/Projects/UOContent/Misc/NameVerification.cs +++ b/Projects/UOContent/Misc/NameVerification.cs @@ -1,247 +1,290 @@ using System; +using System.Buffers; +using System.Runtime.CompilerServices; -namespace Server.Misc +namespace Server.Misc; + +public static class NameVerification { - public static class NameVerification - { - public static readonly char[] SpaceDashPeriodQuote = - { - ' ', '-', '.', '\'' - }; + public static readonly SearchValues AlphaNumeric = SearchValues.Create( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + ); - public static readonly char[] Empty = Array.Empty(); + public static readonly SearchValues Alphabetic = SearchValues.Create( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + ); - public static string[] StartDisallowed { get; } = - { + public static readonly SearchValues Numeric = SearchValues.Create( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + ); + + public static readonly SearchValues SpaceDashPeriodQuote = SearchValues.Create(' ', '-', '.', '\''); + + public static readonly SearchValues StartDisallowed = SearchValues.Create( + [ "seer", "counselor", "gm", "admin", "lady", "lord" - }; + ], + StringComparison.OrdinalIgnoreCase + ); - public static string[] Disallowed { get; } = - { - "jigaboo", - "chigaboo", - "wop", - "kyke", - "kike", - "tit", - "spic", - "prick", - "piss", - "lezbo", - "lesbo", - "felatio", - "dyke", - "dildo", - "chinc", - "chink", - "cunnilingus", - "cum", - "cocksucker", - "cock", - "clitoris", - "clit", - "ass", - "hitler", - "penis", - "nigga", - "nigger", - "klit", - "kunt", - "jiz", - "jism", - "jerkoff", - "jackoff", - "goddamn", - "fag", - "blowjob", - "bitch", - "asshole", - "dick", - "pussy", - "snatch", - "cunt", - "twat", - "shit", - "fuck", - "tailor", - "smith", - "scholar", - "rogue", - "novice", - "neophyte", - "merchant", - "medium", - "master", - "mage", - "lb", - "journeyman", - "grandmaster", - "fisherman", - "expert", - "chef", - "carpenter", - "british", - "blackthorne", - "blackthorn", - "beggar", - "archer", - "apprentice", - "adept", - "gamemaster", - "frozen", - "squelched", - "invulnerable", - "osi", - "origin" - }; + public static readonly string[] Disallowed = + [ + ..ProfanityProtection.Disallowed, + "tailor", + "smith", + "scholar", + "rogue", + "novice", + "neophyte", + "merchant", + "medium", + "master", + "mage", + "lb", + "journeyman", + "grandmaster", + "fisherman", + "expert", + "chef", + "carpenter", + "british", + "blackthorne", + "blackthorn", + "beggar", + "archer", + "apprentice", + "adept", + "gamemaster", + "frozen", + "squelched", + "invulnerable", + "osi", + "origin" + ]; - public static void Configure() + public static readonly SearchValues DisallowedSearchValues = SearchValues.Create( + Disallowed, + StringComparison.OrdinalIgnoreCase + ); + + public static void Configure() + { + CommandSystem.Register("ValidateName", AccessLevel.Administrator, ValidateName_OnCommand); + } + + [Usage("ValidateName"), Description("Checks the result of NameValidation on the specified name.")] + public static void ValidateName_OnCommand(CommandEventArgs e) + { + if (Validate(e.ArgString, 2, 16, true, false, true, 1, SpaceDashPeriodQuote)) { - CommandSystem.Register("ValidateName", AccessLevel.Administrator, ValidateName_OnCommand); + e.Mobile.SendMessage(0x59, "That name is considered valid."); + } + else + { + e.Mobile.SendMessage(0x22, "That name is considered invalid."); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidatePlayerName(ReadOnlySpan name) => + Validate(name, 2, 16, true, false, true, 1, SpaceDashPeriodQuote); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidatePetName(ReadOnlySpan name) => Validate(name, 1, 16, true, false, exceptions: null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ValidateVendorName(ReadOnlySpan name) => Validate(name, 1, 20, true, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Validate( + ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, + bool noExceptionsAtStart = true, int maxExceptions = 0, SearchValues exceptions = null + ) => Validate( + name, + minLength, + maxLength, + allowLetters, + allowDigits, + noExceptionsAtStart, + maxExceptions, + exceptions, + Disallowed, + DisallowedSearchValues, + StartDisallowed + ); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Validate( + ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, + bool noExceptionsAtStart, int maxExceptions, SearchValues exceptions, ReadOnlySpan disallowed, + SearchValues disallowedSV + ) => Validate( + name, + minLength, + maxLength, + allowLetters, + allowDigits, + noExceptionsAtStart, + maxExceptions, + exceptions, + disallowed, + disallowedSV, + null + ); + + public static bool Validate( + ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, + bool noExceptionsAtStart, int maxExceptions, SearchValues exceptions, ReadOnlySpan disallowed, + SearchValues disallowedSV, SearchValues startDisallowedSV + ) + { + if (name.Length == 0 || name.Length < minLength || name.Length > maxLength) + { + return false; } - [Usage("ValidateName"), Description("Checks the result of NameValidation on the specified name.")] - public static void ValidateName_OnCommand(CommandEventArgs e) + if (exceptions == null) { - if (Validate(e.ArgString, 2, 16, true, false, true, 1, SpaceDashPeriodQuote)) + // We don't have exceptions, so we might be limited to letters or numbers + var allowed = allowLetters switch { - e.Mobile.SendMessage(0x59, "That name is considered valid."); - } - else + // If we don't allow exceptions, then non-alphanumeric is not allowed + true when allowDigits && maxExceptions == 0 => AlphaNumeric, + true when !allowDigits => Alphabetic, + false when allowDigits => Numeric, + // Everything has been allowed! Use `Utility.FixHtml()` to stop weird behavior + _ => null + }; + + if (allowed != null && name.ContainsAnyExcept(allowed)) { - e.Mobile.SendMessage(0x22, "That name is considered invalid."); + return false; } } - - public static bool Validate( - string name, int minLength, int maxLength, bool allowLetters, bool allowDigits, - bool noExceptionsAtStart, int maxExceptions, char[] exceptions - ) => - Validate( - name, - minLength, - maxLength, - allowLetters, - allowDigits, - noExceptionsAtStart, - maxExceptions, - exceptions, - Disallowed, - StartDisallowed - ); - - public static bool Validate( - string name, int minLength, int maxLength, bool allowLetters, bool allowDigits, - bool noExceptionsAtStart, int maxExceptions, char[] exceptions, string[] disallowed, string[] startDisallowed - ) + else { - if (name == null || name.Length < minLength || name.Length > maxLength) + // We have exceptions, and at least one of the letters/digits flag is false: + var notAllowed = allowLetters switch + { + true when !allowDigits => Numeric, + false when allowDigits => Alphabetic, + _ => null + }; + + if (notAllowed != null && name.ContainsAny(notAllowed)) { return false; } - var exceptCount = 0; - - name = name.ToLower(); - - if (!allowLetters || !allowDigits || - exceptions.Length > 0 && (noExceptionsAtStart || maxExceptions < int.MaxValue)) + if (ContainsExceptions(name, exceptions, noExceptionsAtStart, maxExceptions)) { - for (var i = 0; i < name.Length; ++i) - { - var c = name[i]; - - if (c >= 'a' && c <= 'z') - { - if (!allowLetters) - { - return false; - } - - exceptCount = 0; - } - else if (c >= '0' && c <= '9') - { - if (!allowDigits) - { - return false; - } - - exceptCount = 0; - } - else - { - var except = false; - - for (var j = 0; !except && j < exceptions.Length; ++j) - { - if (c == exceptions[j]) - { - except = true; - } - } - - if (!except || i == 0 && noExceptionsAtStart) - { - return false; - } - - if (exceptCount++ == maxExceptions) - { - return false; - } - } - } + return false; } - - for (var i = 0; i < disallowed.Length; ++i) - { - var indexOf = name.IndexOfOrdinal(disallowed[i]); - - if (indexOf == -1) - { - continue; - } - - var badPrefix = indexOf == 0; - - for (var j = 0; !badPrefix && j < exceptions.Length; ++j) - { - badPrefix = name[indexOf - 1] == exceptions[j]; - } - - if (!badPrefix) - { - continue; - } - - var badSuffix = indexOf + disallowed[i].Length >= name.Length; - - for (var j = 0; !badSuffix && j < exceptions.Length; ++j) - { - badSuffix = name[indexOf + disallowed[i].Length] == exceptions[j]; - } - - if (badSuffix) - { - return false; - } - } - - for (var i = 0; i < startDisallowed.Length; ++i) - { - if (name.StartsWithOrdinal(startDisallowed[i])) - { - return false; - } - } - - return true; } + + if (disallowedSV != null && disallowed.Length > 0 && ContainsDisallowedWord(name, disallowed, disallowedSV)) + { + return false; + } + + return startDisallowedSV == null || name.IndexOfAny(startDisallowedSV) != 0; + } + + public static bool ContainsExceptions( + ReadOnlySpan name, SearchValues exceptions, bool noExceptionsAtStart, int maxExceptions + ) + { + if (!noExceptionsAtStart && maxExceptions is <= -1 or >= int.MaxValue) + { + return false; + } + + var index = name.IndexOfAny(exceptions); + + while (index != -1) + { + if (noExceptionsAtStart) + { + if (index == 0) + { + return true; + } + + noExceptionsAtStart = false; + } + + if (maxExceptions-- <= 0) + { + return true; + } + + if (index + 1 < name.Length) + { + name = name[(index + 1)..]; + index = name.IndexOfAny(exceptions); + } + else + { + index = -1; + } + } + + return false; + } + + public static bool ContainsDisallowedWord(ReadOnlySpan name, ReadOnlySpan disallowed, SearchValues disallowedSV) + { + var index = name.IndexOfAny(disallowedSV); + + while (index != -1) + { + var isStartBoundary = index == 0 || !char.IsLetterOrDigit(name[index - 1]); + + if (isStartBoundary) + { + for (var i = 0; i < disallowed.Length; i++) + { + var word = disallowed[i].AsSpan(); + if (index + word.Length > name.Length || !name.Slice(index, word.Length).InsensitiveEquals(word)) + { + continue; + } + + // End boundary + if (index + word.Length == name.Length || !char.IsLetterOrDigit(name[index + word.Length])) + { + return true; + } + } + } + + if (index + 1 < name.Length) + { + name = name[(index + 1)..]; + index = name.IndexOfAny(disallowedSV); + } + else + { + index = -1; + } + } + + return false; } } diff --git a/Projects/UOContent/Misc/ProfanityProtection.cs b/Projects/UOContent/Misc/ProfanityProtection.cs index cf917938e..9aa0280d4 100644 --- a/Projects/UOContent/Misc/ProfanityProtection.cs +++ b/Projects/UOContent/Misc/ProfanityProtection.cs @@ -1,140 +1,147 @@ using System; +using System.Buffers; -namespace Server.Misc +namespace Server.Misc; + +public enum ProfanityAction { - public enum ProfanityAction + None, // no action taken + Disallow, // speech is not displayed + Criminal, // makes the player criminal, not killable by guards + CriminalAction, // makes the player criminal, can be killed by guards + Disconnect, // player is kicked + Other // some other implementation +} + +public static class ProfanityProtection +{ + private static bool Enabled; + private static ProfanityAction Action; + + public static void Configure() { - None, // no action taken - Disallow, // speech is not displayed - Criminal, // makes the player criminal, not killable by guards - CriminalAction, // makes the player criminal, can be killed by guards - Disconnect, // player is kicked - Other // some other implementation + Enabled = ServerConfiguration.GetSetting("profanityProtection.enabled", false); + Action = ServerConfiguration.GetSetting("profanityProtection.action", ProfanityAction.Disallow); + + if (Enabled) + { + EventSink.Speech += EventSink_Speech; + } } - public static class ProfanityProtection + // Used by the guild system + public static readonly SearchValues Exceptions = SearchValues.Create( + ' ', '-', '.', '\'', '"', ',', '_', '+', '=', '~', '`', '!', '^', '*', '\\', '/', ';', ':', '<', '>', '[', ']', + '{', '}', '?', '|', '(', ')', '%', '$', '&', '#', '@' + ); + + public static readonly string[] Disallowed = + [ + "jigaboo", + "chigaboo", + "wop", + "kyke", + "kike", + "tit", + "spic", + "prick", + "piss", + "lezbo", + "lesbo", + "felatio", + "dyke", + "dildo", + "chinc", + "chink", + "cunnilingus", + "cum", + "cocksucker", + "cock", + "clitoris", + "clit", + "ass", + "hitler", + "penis", + "nigga", + "nigger", + "klit", + "kunt", + "jiz", + "jism", + "jerkoff", + "jackoff", + "goddamn", + "fag", + "blowjob", + "bitch", + "asshole", + "dick", + "pussy", + "snatch", + "cunt", + "twat", + "shit", + "fuck", + ]; + + public static readonly SearchValues DisallowedSearchValues = SearchValues.Create( + Disallowed, + StringComparison.OrdinalIgnoreCase + ); + + private static bool OnProfanityDetected(Mobile from, string speech) { - // TODO: Move this to configuration - private static readonly bool Enabled = false; - - private static readonly ProfanityAction - Action = ProfanityAction.Disallow; // change here what to do when profanity is detected - - public static char[] Exceptions { get; } = + switch (Action) { - ' ', '-', '.', '\'', '"', ',', '_', '+', '=', '~', '`', '!', '^', '*', '\\', '/', ';', ':', '<', '>', '[', ']', - '{', '}', '?', '|', '(', ')', '%', '$', '&', '#', '@' - }; + case ProfanityAction.None: return true; + case ProfanityAction.Disallow: return false; + case ProfanityAction.Criminal: + from.Criminal = true; + return true; + case ProfanityAction.CriminalAction: + from.CriminalAction(false); + return true; + case ProfanityAction.Disconnect: + { + from.NetState?.Disconnect("Using profanity."); - public static string[] StartDisallowed { get; } = Array.Empty(); + return false; + } + default: + case ProfanityAction.Other: // TODO: Provide custom implementation if this is chosen + { + return true; + } + } + } - public static string[] Disallowed { get; } = + public static bool ContainsProfanity(ReadOnlySpan speech) => + speech.Length > 0 && + !NameVerification.Validate( + speech, + 1, + int.MaxValue, + true, + true, + true, + int.MaxValue, // allow all non-alphanumeric characters + null, + Disallowed, + DisallowedSearchValues + ); + + private static void EventSink_Speech(SpeechEventArgs e) + { + var from = e.Mobile; + + if (from.AccessLevel > AccessLevel.Player) { - "jigaboo", - "chigaboo", - "wop", - "kyke", - "kike", - "tit", - "spic", - "prick", - "piss", - "lezbo", - "lesbo", - "felatio", - "dyke", - "dildo", - "chinc", - "chink", - "cunnilingus", - "cum", - "cocksucker", - "cock", - "clitoris", - "clit", - "ass", - "hitler", - "penis", - "nigga", - "nigger", - "klit", - "kunt", - "jiz", - "jism", - "jerkoff", - "jackoff", - "goddamn", - "fag", - "blowjob", - "bitch", - "asshole", - "dick", - "pussy", - "snatch", - "cunt", - "twat", - "shit", - "fuck" - }; - - public static void Initialize() - { - if (Enabled) - { - EventSink.Speech += EventSink_Speech; - } + return; } - private static bool OnProfanityDetected(Mobile from, string speech) + if (ContainsProfanity(e.Speech)) { - switch (Action) - { - case ProfanityAction.None: return true; - case ProfanityAction.Disallow: return false; - case ProfanityAction.Criminal: - from.Criminal = true; - return true; - case ProfanityAction.CriminalAction: - from.CriminalAction(false); - return true; - case ProfanityAction.Disconnect: - { - from.NetState?.Disconnect("Using profanity."); - - return false; - } - default: - case ProfanityAction.Other: // TODO: Provide custom implementation if this is chosen - { - return true; - } - } - } - - private static void EventSink_Speech(SpeechEventArgs e) - { - var from = e.Mobile; - - if (from.AccessLevel > AccessLevel.Player) - { - return; - } - - if (!NameVerification.Validate( - e.Speech, - 0, - int.MaxValue, - true, - true, - false, - int.MaxValue, - Exceptions, - Disallowed, - StartDisallowed - )) - { - e.Blocked = !OnProfanityDetected(from, e.Speech); - } + e.Blocked = !OnProfanityDetected(from, e.Speech); } } } diff --git a/Projects/UOContent/Misc/RenameRequests.cs b/Projects/UOContent/Misc/RenameRequests.cs index ffaece37e..8abf648d7 100644 --- a/Projects/UOContent/Misc/RenameRequests.cs +++ b/Projects/UOContent/Misc/RenameRequests.cs @@ -1,54 +1,31 @@ using System; -namespace Server.Misc +namespace Server.Misc; + +public static class RenameRequests { - public static class RenameRequests + public static void RenameRequest(Mobile from, Mobile targ, string name) { - public static void RenameRequest(Mobile from, Mobile targ, string name) + if (!from.CanSee(targ) || !from.InRange(targ, 12) || !targ.CanBeRenamedBy(from)) { - if (from.CanSee(targ) && from.InRange(targ, 12) && targ.CanBeRenamedBy(from)) - { - name = name.Trim(); + return; + } - if (NameVerification.Validate( - name, - 1, - 16, - true, - false, - true, - 0, - NameVerification.Empty, - NameVerification.StartDisallowed, - Core.ML ? NameVerification.Disallowed : Array.Empty() - )) - { - if (Core.ML) - { - var disallowed = ProfanityProtection.Disallowed; + var span = name.AsSpan().Trim(); - for (var i = 0; i < disallowed.Length; i++) - { - if (name.IndexOfOrdinal(disallowed[i]) != -1) - { - from.SendLocalizedMessage(1072622); // That name isn't very polite. - return; - } - } - - from.SendLocalizedMessage( - 1072623, - $"{targ.Name}\t{name}" - ); // Pet ~1_OLDPETNAME~ renamed to ~2_NEWPETNAME~. - } - - targ.Name = name; - } - else - { - from.SendMessage("That name is unacceptable."); - } - } + if (NameVerification.ValidatePetName(span)) + { + // Pet ~1_OLDPETNAME~ renamed to ~2_NEWPETNAME~. + from.SendLocalizedMessage(1072623, $"{targ.Name}\t{span}"); + targ.Name = span.ToString(); + } + else if (span.IndexOfAny(ProfanityProtection.DisallowedSearchValues) != -1) + { + from.SendLocalizedMessage(1072622); // That name isn't very polite. + } + else + { + from.SendMessage("That name is unacceptable."); } } } diff --git a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs index 2721fadbd..d95bcf972 100644 --- a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs @@ -1232,9 +1232,9 @@ public partial class PlayerVendor : Mobile return; } - var name = text.Trim(); + var name = text.AsSpan().Trim(); - if (!NameVerification.Validate(name, 1, 20, true, true, true, 0, NameVerification.Empty)) + if (!NameVerification.ValidateVendorName(name)) { m_Vendor.SayTo(from, "That name is unacceptable."); return; @@ -1261,9 +1261,9 @@ public partial class PlayerVendor : Mobile return; } - var name = text.Trim(); + var name = text.AsSpan().Trim(); - if (!NameVerification.Validate(name, 1, 20, true, true, true, 0, NameVerification.Empty)) + if (!NameVerification.ValidateVendorName(name)) { m_Vendor.SayTo(from, "That name is unacceptable."); return;