refactor: split StringExtensions into 5 partial class files
Split the 1,600-line StringExtensions.cs into logical categories: - StringExtensions.Culture.cs - invariant comparison methods - StringExtensions.Manipulation.cs - string modification methods - StringExtensions.Encoding.cs - hashing, base64, guid encoding - StringExtensions.Parsing.cs - parsing and detection methods - StringExtensions.Sanitization.cs - XSS, HTML, file path methods No functional changes. All existing tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
75
src/Umbraco.Core/Extensions/StringExtensions.Culture.cs
Normal file
75
src/Umbraco.Core/Extensions/StringExtensions.Culture.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Culture and invariant comparison extensions.
|
||||
/// </summary>
|
||||
public static partial class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// formats the string with invariant culture
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns></returns>
|
||||
public static string InvariantFormat(this string? format, params object?[] args) =>
|
||||
string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an integer to an invariant formatted string
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToInvariantString(this int str) => str.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
public static string ToInvariantString(this long str) => str.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Compares 2 strings with invariant culture and case ignored
|
||||
/// </summary>
|
||||
/// <param name="compare">The compare.</param>
|
||||
/// <param name="compareTo">The compare to.</param>
|
||||
/// <returns></returns>
|
||||
public static bool InvariantEquals(this string? compare, string? compareTo) =>
|
||||
string.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
public static bool InvariantStartsWith(this string compare, string compareTo) =>
|
||||
compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
public static bool InvariantEndsWith(this string compare, string compareTo) =>
|
||||
compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
public static bool InvariantContains(this string compare, string compareTo) =>
|
||||
compare.Contains(compareTo, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool InvariantContains(this IEnumerable<string> compare, string compareTo) =>
|
||||
compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
public static int InvariantIndexOf(this string s, string value) =>
|
||||
s.IndexOf(value, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static int InvariantLastIndexOf(this string s, string value) =>
|
||||
s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the provided string is a valid culture code and returns it in a consistent casing.
|
||||
/// </summary>
|
||||
/// <param name="culture">Culture code.</param>
|
||||
/// <returns>Culture code in standard casing.</returns>
|
||||
public static string? EnsureCultureCode(this string? culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(culture) || culture == "*")
|
||||
{
|
||||
return culture;
|
||||
}
|
||||
|
||||
// Create as CultureInfo instance from provided name so we can ensure consistent casing of culture code when persisting.
|
||||
// This will accept mixed case but once created have a `Name` property that is consistently and correctly cased.
|
||||
// Will throw in an invalid culture code is provided.
|
||||
return new CultureInfo(culture).Name;
|
||||
}
|
||||
}
|
||||
485
src/Umbraco.Core/Extensions/StringExtensions.Encoding.cs
Normal file
485
src/Umbraco.Core/Extensions/StringExtensions.Encoding.cs
Normal file
@@ -0,0 +1,485 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Encoding, hashing, and serialization extensions.
|
||||
/// </summary>
|
||||
public static partial class StringExtensions
|
||||
{
|
||||
private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray();
|
||||
private static readonly char[] ToCSharpEscapeChars;
|
||||
|
||||
/// <summary>
|
||||
/// The namespace for URLs (from RFC 4122, Appendix C).
|
||||
/// See <a href="http://www.ietf.org/rfc/rfc4122.txt">RFC 4122</a>
|
||||
/// </summary>
|
||||
internal static readonly Guid UrlNamespace = new("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
|
||||
|
||||
static StringExtensions()
|
||||
{
|
||||
var escapes = new[] { "\aa", "\bb", "\ff", "\nn", "\rr", "\tt", "\vv", "\"\"", "\\\\", "??", "\00" };
|
||||
ToCSharpEscapeChars = new char[escapes.Max(e => e[0]) + 1];
|
||||
foreach (var escape in escapes)
|
||||
{
|
||||
ToCSharpEscapeChars[escape[0]] = escape[1];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a hash of a string based on the FIPS compliance setting.
|
||||
/// </summary>
|
||||
/// <param name="str">Refers to itself</param>
|
||||
/// <returns>The hashed string</returns>
|
||||
public static string GenerateHash(this string str) => str.ToSHA1();
|
||||
|
||||
/// <summary>
|
||||
/// Generate a hash of a string based on the specified hash algorithm.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The hash algorithm implementation to use.</typeparam>
|
||||
/// <param name="str">The <see cref="string" /> to hash.</param>
|
||||
/// <returns>
|
||||
/// The hashed string.
|
||||
/// </returns>
|
||||
public static string GenerateHash<T>(this string str)
|
||||
where T : HashAlgorithm => str.GenerateHash(typeof(T).FullName);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the string to SHA1
|
||||
/// </summary>
|
||||
/// <param name="stringToConvert">refers to itself</param>
|
||||
/// <returns>The SHA1 hashed string</returns>
|
||||
public static string ToSHA1(this string stringToConvert) => stringToConvert.GenerateHash("SHA1");
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string to a safe URL base64 string
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToUrlBase64(this string input)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return UrlTokenEncode(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a URL safe base64 string back
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static string? FromUrlBase64(this string input)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
// if (input.IsInvalidBase64()) return null;
|
||||
try
|
||||
{
|
||||
// var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "="));
|
||||
var decodedBytes = UrlTokenDecode(input);
|
||||
return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string so that it is 'safe' for URLs, files, etc..
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static string UrlTokenEncode(this byte[] input)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
if (input.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// base-64 digits are A-Z, a-z, 0-9, + and /
|
||||
// the = char is used for trailing padding
|
||||
var str = Convert.ToBase64String(input);
|
||||
|
||||
var pos = str.IndexOf('=');
|
||||
if (pos < 0)
|
||||
{
|
||||
pos = str.Length;
|
||||
}
|
||||
|
||||
// replace chars that would cause problems in URLs
|
||||
Span<char> chArray = pos <= 1024 ? stackalloc char[pos] : new char[pos];
|
||||
for (var i = 0; i < pos; i++)
|
||||
{
|
||||
var ch = str[i];
|
||||
switch (ch)
|
||||
{
|
||||
case '+': // replace '+' with '-'
|
||||
chArray[i] = '-';
|
||||
break;
|
||||
|
||||
case '/': // replace '/' with '_'
|
||||
chArray[i] = '_';
|
||||
break;
|
||||
|
||||
default: // keep char unchanged
|
||||
chArray[i] = ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chArray);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a string that was encoded with UrlTokenEncode
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] UrlTokenDecode(this string input)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
if (input.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// calc array size - must be groups of 4
|
||||
var arrayLength = input.Length;
|
||||
var remain = arrayLength % 4;
|
||||
if (remain != 0)
|
||||
{
|
||||
arrayLength += 4 - remain;
|
||||
}
|
||||
|
||||
var inArray = new char[arrayLength];
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var ch = input[i];
|
||||
switch (ch)
|
||||
{
|
||||
case '-': // restore '-' as '+'
|
||||
inArray[i] = '+';
|
||||
break;
|
||||
|
||||
case '_': // restore '_' as '/'
|
||||
inArray[i] = '/';
|
||||
break;
|
||||
|
||||
default: // keep char unchanged
|
||||
inArray[i] = ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// pad with '='
|
||||
for (var j = input.Length; j < inArray.Length; j++)
|
||||
{
|
||||
inArray[j] = '=';
|
||||
}
|
||||
|
||||
return Convert.FromBase64CharArray(inArray, 0, inArray.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts to hex.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns></returns>
|
||||
public static string ConvertToHex(this string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(input.Length);
|
||||
foreach (var c in input)
|
||||
{
|
||||
sb.AppendFormat("{0:x2}", Convert.ToUInt32(c));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string DecodeFromHex(this string hexValue)
|
||||
{
|
||||
var strValue = string.Empty;
|
||||
while (hexValue.Length > 0)
|
||||
{
|
||||
strValue += Convert.ToChar(Convert.ToUInt32(hexValue[..2], 16)).ToString();
|
||||
hexValue = hexValue[2..];
|
||||
}
|
||||
|
||||
return strValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes as GUID.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns></returns>
|
||||
public static Guid EncodeAsGuid(this string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
throw new ArgumentNullException("input");
|
||||
}
|
||||
|
||||
var convertToHex = input.ConvertToHex();
|
||||
var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32;
|
||||
var hex = convertToHex[..hexLength].PadLeft(32, '0');
|
||||
return Guid.TryParse(hex, out Guid output) ? output : Guid.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to a Guid - WARNING, depending on the string, this may not be unique
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public static Guid ToGuid(this string text) =>
|
||||
CreateGuidFromHash(
|
||||
UrlNamespace,
|
||||
text,
|
||||
CryptoConfig.AllowOnlyFipsAlgorithms ? 5 // SHA1
|
||||
: 3); // MD5
|
||||
|
||||
/// <summary>
|
||||
/// Creates a name-based UUID using the algorithm from RFC 4122 §4.3.
|
||||
/// See
|
||||
/// <a href="https://github.com/LogosBible/Logos.Utility/blob/master/src/Logos.Utility/GuidUtility.cs#L34">GuidUtility.cs</a>
|
||||
/// for original implementation.
|
||||
/// </summary>
|
||||
/// <param name="namespaceId">The ID of the namespace.</param>
|
||||
/// <param name="name">The name (within that namespace).</param>
|
||||
/// <param name="version">
|
||||
/// The version number of the UUID to create; this value must be either
|
||||
/// 3 (for MD5 hashing) or 5 (for SHA-1 hashing).
|
||||
/// </param>
|
||||
/// <returns>A UUID derived from the namespace and name.</returns>
|
||||
/// <remarks>
|
||||
/// See
|
||||
/// <a href="http://code.logos.com/blog/2011/04/generating_a_deterministic_guid.html">Generating a deterministic GUID</a>
|
||||
/// .
|
||||
/// </remarks>
|
||||
internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException("name");
|
||||
}
|
||||
|
||||
if (version != 3 && version != 5)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5.");
|
||||
}
|
||||
|
||||
// convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3)
|
||||
// ASSUME: UTF-8 encoding is always appropriate
|
||||
var nameBytes = Encoding.UTF8.GetBytes(name);
|
||||
|
||||
// convert the namespace UUID to network order (step 3)
|
||||
var namespaceBytes = namespaceId.ToByteArray();
|
||||
SwapByteOrder(namespaceBytes);
|
||||
|
||||
// comput the hash of the name space ID concatenated with the name (step 4)
|
||||
byte[] hash;
|
||||
using (HashAlgorithm algorithm = version == 3 ? MD5.Create() : SHA1.Create())
|
||||
{
|
||||
algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0);
|
||||
algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length);
|
||||
hash = algorithm.Hash!;
|
||||
}
|
||||
|
||||
// most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12)
|
||||
Span<byte> newGuid = hash.AsSpan()[..16];
|
||||
|
||||
// set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8)
|
||||
newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4));
|
||||
|
||||
// set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10)
|
||||
newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80);
|
||||
|
||||
// convert the resulting UUID to local byte order (step 13)
|
||||
SwapByteOrder(newGuid);
|
||||
return new Guid(newGuid);
|
||||
}
|
||||
|
||||
// Converts a GUID (expressed as a byte array) to/from network order (MSB-first).
|
||||
internal static void SwapByteOrder(Span<byte> guid)
|
||||
{
|
||||
SwapBytes(guid, 0, 3);
|
||||
SwapBytes(guid, 1, 2);
|
||||
SwapBytes(guid, 4, 5);
|
||||
SwapBytes(guid, 6, 7);
|
||||
}
|
||||
|
||||
private static void SwapBytes(Span<byte> guid, int left, int right) => (guid[left], guid[right]) = (guid[right], guid[left]);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a literal string into a C# expression.
|
||||
/// </summary>
|
||||
/// <param name="s">Current instance of the string.</param>
|
||||
/// <returns>The string in a C# format.</returns>
|
||||
public static string ToCSharpString(this string s)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
return "<null>";
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal
|
||||
var sb = new StringBuilder(s.Length + 2);
|
||||
for (var rp = 0; rp < s.Length; rp++)
|
||||
{
|
||||
var c = s[rp];
|
||||
if (c < ToCSharpEscapeChars.Length && ToCSharpEscapeChars[c] != '\0')
|
||||
{
|
||||
sb.Append('\\').Append(ToCSharpEscapeChars[c]);
|
||||
}
|
||||
else if (c <= '~' && c >= ' ')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(@"\x")
|
||||
.Append(ToCSharpHexDigitLower[(c >> 12) & 0x0F])
|
||||
.Append(ToCSharpHexDigitLower[(c >> 8) & 0x0F])
|
||||
.Append(ToCSharpHexDigitLower[(c >> 4) & 0x0F])
|
||||
.Append(ToCSharpHexDigitLower[c & 0x0F]);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
|
||||
// requires full trust
|
||||
/*
|
||||
using (var writer = new StringWriter())
|
||||
using (var provider = CodeDomProvider.CreateProvider("CSharp"))
|
||||
{
|
||||
provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null);
|
||||
return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), "");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public static string EncodeJsString(this string s)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in s)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\"':
|
||||
sb.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
sb.Append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
sb.Append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
sb.Append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
sb.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
sb.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
sb.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
int i = c;
|
||||
if (i < 32 || i > 127)
|
||||
{
|
||||
sb.AppendFormat("\\u{0:X04}", i);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a hash of a string based on the hashType passed in
|
||||
/// </summary>
|
||||
/// <param name="str">Refers to itself</param>
|
||||
/// <param name="hashType">
|
||||
/// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a
|
||||
/// list of possible values.
|
||||
/// </param>
|
||||
/// <returns>The hashed string</returns>
|
||||
private static string GenerateHash(this string str, string? hashType)
|
||||
{
|
||||
HashAlgorithm? hasher = null;
|
||||
|
||||
// create an instance of the correct hashing provider based on the type passed in
|
||||
if (hashType is not null)
|
||||
{
|
||||
hasher = HashAlgorithm.Create(hashType);
|
||||
}
|
||||
|
||||
if (hasher == null)
|
||||
{
|
||||
throw new InvalidOperationException("No hashing type found by name " + hashType);
|
||||
}
|
||||
|
||||
using (hasher)
|
||||
{
|
||||
// convert our string into byte array
|
||||
var byteArray = Encoding.UTF8.GetBytes(str);
|
||||
|
||||
// get the hashed values created by our selected provider
|
||||
var hashedByteArray = hasher.ComputeHash(byteArray);
|
||||
|
||||
// create a StringBuilder object
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
// loop to each byte
|
||||
foreach (var b in hashedByteArray)
|
||||
{
|
||||
// append it to our StringBuilder
|
||||
stringBuilder.Append(b.ToString("x2"));
|
||||
}
|
||||
|
||||
// return the hashed value
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
615
src/Umbraco.Core/Extensions/StringExtensions.Manipulation.cs
Normal file
615
src/Umbraco.Core/Extensions/StringExtensions.Manipulation.cs
Normal file
@@ -0,0 +1,615 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// String manipulation and modification extensions.
|
||||
/// </summary>
|
||||
public static partial class StringExtensions
|
||||
{
|
||||
internal static readonly Lazy<Regex> Whitespace = new(() => new Regex(@"\s+", RegexOptions.Compiled));
|
||||
|
||||
/// <summary>
|
||||
/// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts
|
||||
/// char or char[].
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="forRemoving">For removing.</param>
|
||||
/// <returns></returns>
|
||||
public static string Trim(this string value, string forRemoving)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.TrimEnd(forRemoving).TrimStart(forRemoving);
|
||||
}
|
||||
|
||||
public static string TrimEnd(this string value, string forRemoving)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(forRemoving))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static string TrimStart(this string value, string forRemoving)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(forRemoving))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
value = value[forRemoving.Length..];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static string EnsureStartsWith(this string input, string toStartWith)
|
||||
{
|
||||
if (input.StartsWith(toStartWith))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return toStartWith + input.TrimStart(toStartWith);
|
||||
}
|
||||
|
||||
public static string EnsureStartsWith(this string input, char value) =>
|
||||
input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input;
|
||||
|
||||
public static string EnsureEndsWith(this string input, char value) =>
|
||||
input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value;
|
||||
|
||||
public static string EnsureEndsWith(this string input, string toEndWith) =>
|
||||
input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to uppercase.
|
||||
/// </summary>
|
||||
/// <param name="input">The string.</param>
|
||||
/// <returns>The converted string.</returns>
|
||||
public static string ToFirstUpper(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input[..1].ToUpper() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to lowercase.
|
||||
/// </summary>
|
||||
/// <param name="input">The string.</param>
|
||||
/// <returns>The converted string.</returns>
|
||||
public static string ToFirstLower(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input[..1].ToLower() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
|
||||
/// specified culture.
|
||||
/// </summary>
|
||||
/// <param name="input">The string.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The converted string.</returns>
|
||||
public static string ToFirstUpper(this string input, CultureInfo culture) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input[..1].ToUpper(culture) + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
|
||||
/// specified culture.
|
||||
/// </summary>
|
||||
/// <param name="input">The string.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The converted string.</returns>
|
||||
public static string ToFirstLower(this string input, CultureInfo culture) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input[..1].ToLower(culture) + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
|
||||
/// invariant culture.
|
||||
/// </summary>
|
||||
/// <param name="input">The string.</param>
|
||||
/// <returns>The converted string.</returns>
|
||||
public static string ToFirstUpperInvariant(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input[..1].ToUpperInvariant() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
|
||||
/// invariant culture.
|
||||
/// </summary>
|
||||
/// <param name="input">The string.</param>
|
||||
/// <returns>The converted string.</returns>
|
||||
public static string ToFirstLowerInvariant(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input[..1].ToLowerInvariant() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string in which all occurrences of specified strings are replaced by other specified strings.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to filter.</param>
|
||||
/// <param name="replacements">The replacements definition.</param>
|
||||
/// <returns>The filtered string.</returns>
|
||||
public static string ReplaceMany(this string text, IDictionary<string, string> replacements)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
if (replacements == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(replacements));
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> item in replacements)
|
||||
{
|
||||
text = text.Replace(item.Key, item.Value);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string in which all occurrences of specified characters are replaced by a specified character.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to filter.</param>
|
||||
/// <param name="chars">The characters to replace.</param>
|
||||
/// <param name="replacement">The replacement character.</param>
|
||||
/// <returns>The filtered string.</returns>
|
||||
public static string ReplaceMany(this string text, char[] chars, char replacement)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
if (chars == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(chars));
|
||||
}
|
||||
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
{
|
||||
text = text.Replace(chars[i], replacement);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string in which only the first occurrence of a specified string is replaced by a specified
|
||||
/// replacement string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to filter.</param>
|
||||
/// <param name="search">The string to replace.</param>
|
||||
/// <param name="replace">The replacement string.</param>
|
||||
/// <returns>The filtered string.</returns>
|
||||
public static string ReplaceFirst(this string text, string search, string replace)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> spanText = text.AsSpan();
|
||||
var pos = spanText.IndexOf(search, StringComparison.InvariantCulture);
|
||||
|
||||
if (pos < 0)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return string.Concat(spanText[..pos], replace.AsSpan(), spanText[(pos + search.Length)..]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An extension method that returns a new string in which all occurrences of a
|
||||
/// specified string in the current instance are replaced with another specified string.
|
||||
/// StringComparison specifies the type of search to use for the specified string.
|
||||
/// </summary>
|
||||
/// <param name="source">Current instance of the string</param>
|
||||
/// <param name="oldString">Specified string to replace</param>
|
||||
/// <param name="newString">Specified string to inject</param>
|
||||
/// <param name="stringComparison">String Comparison object to specify search type</param>
|
||||
/// <returns>Updated string</returns>
|
||||
public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison)
|
||||
{
|
||||
// This initialization ensures the first check starts at index zero of the source. On successive checks for
|
||||
// a match, the source is skipped to immediately after the last replaced occurrence for efficiency
|
||||
// and to avoid infinite loops when oldString and newString compare equal.
|
||||
var index = -1 * newString.Length;
|
||||
|
||||
// Determine if there are any matches left in source, starting from just after the result of replacing the last match.
|
||||
while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0)
|
||||
{
|
||||
// Remove the old text.
|
||||
source = source.Remove(index, oldString.Length);
|
||||
|
||||
// Add the replacement text.
|
||||
source = source.Insert(index, newString);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
public static string ReplaceNonAlphanumericChars(this string input, string replacement)
|
||||
{
|
||||
// any character that is not alphanumeric, convert to a hyphen
|
||||
var mName = input;
|
||||
foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c)))
|
||||
{
|
||||
mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement);
|
||||
}
|
||||
|
||||
return mName;
|
||||
}
|
||||
|
||||
public static string ReplaceNonAlphanumericChars(this string input, char replacement)
|
||||
{
|
||||
var chars = input.ToCharArray();
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(chars[i]))
|
||||
{
|
||||
chars[i] = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
public static string ExceptChars(this string str, HashSet<char> toExclude)
|
||||
{
|
||||
var sb = new StringBuilder(str.Length);
|
||||
foreach (var c in str.Where(c => toExclude.Contains(c) == false))
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates the specified text string.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="maxLength">Length of the max.</param>
|
||||
/// <param name="suffix">The suffix.</param>
|
||||
/// <returns></returns>
|
||||
public static string Truncate(this string text, int maxLength, string suffix = "...")
|
||||
{
|
||||
// replaces the truncated string to a ...
|
||||
var truncatedString = text;
|
||||
|
||||
if (maxLength <= 0)
|
||||
{
|
||||
return truncatedString;
|
||||
}
|
||||
|
||||
var strLength = maxLength - suffix.Length;
|
||||
|
||||
if (strLength <= 0)
|
||||
{
|
||||
return truncatedString;
|
||||
}
|
||||
|
||||
if (text == null || text.Length <= maxLength)
|
||||
{
|
||||
return truncatedString;
|
||||
}
|
||||
|
||||
truncatedString = text[..strLength];
|
||||
truncatedString = truncatedString.TrimEnd();
|
||||
truncatedString += suffix;
|
||||
|
||||
return truncatedString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes new lines and tabs
|
||||
/// </summary>
|
||||
/// <param name="txt"></param>
|
||||
/// <returns></returns>
|
||||
public static string StripWhitespace(this string txt) => Regex.Replace(txt, @"\s", string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Strips carrage returns and line feeds from the specified text.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns></returns>
|
||||
public static string StripNewLines(this string input) => input.Replace("\r", string.Empty).Replace("\n", string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Converts to single line by replacing line breaks with spaces.
|
||||
/// </summary>
|
||||
public static string ToSingleLine(this string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
text = text.Replace("\r\n", " "); // remove CRLF
|
||||
text = text.Replace("\r", " "); // remove CR
|
||||
text = text.Replace("\n", " "); // remove LF
|
||||
return text;
|
||||
}
|
||||
|
||||
// this is from SqlMetal and just makes it a bit of fun to allow pluralization
|
||||
public static string MakePluralName(this string name)
|
||||
{
|
||||
if (name.EndsWith("x", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.EndsWith("ch", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.EndsWith("s", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name += "es";
|
||||
return name;
|
||||
}
|
||||
|
||||
if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && name.Length > 1 &&
|
||||
!IsVowel(name[^2]))
|
||||
{
|
||||
name = name.Remove(name.Length - 1, 1);
|
||||
name += "ies";
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name += "s";
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public static bool IsVowel(this char c)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'O':
|
||||
case 'U':
|
||||
case 'Y':
|
||||
case 'A':
|
||||
case 'E':
|
||||
case 'I':
|
||||
case 'o':
|
||||
case 'u':
|
||||
case 'y':
|
||||
case 'a':
|
||||
case 'e':
|
||||
case 'i':
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsLowerCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) ==
|
||||
ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
|
||||
public static bool IsUpperCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) ==
|
||||
ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant();
|
||||
|
||||
// FORMAT STRINGS
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string to produce a string that can safely be used in an alias.
|
||||
/// </summary>
|
||||
/// <param name="alias">The text to filter.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <returns>The safe alias.</returns>
|
||||
public static string ToSafeAlias(this string alias, IShortStringHelper? shortStringHelper) =>
|
||||
shortStringHelper?.CleanStringForSafeAlias(alias) ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string to produce a string that can safely be used in an alias.
|
||||
/// </summary>
|
||||
/// <param name="alias">The text to filter.</param>
|
||||
/// <param name="camel">A value indicating that we want to camel-case the alias.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <returns>The safe alias.</returns>
|
||||
public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, bool camel)
|
||||
{
|
||||
var a = shortStringHelper.CleanStringForSafeAlias(alias);
|
||||
if (string.IsNullOrWhiteSpace(a) || camel == false)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
return char.ToLowerInvariant(a[0]) + a[1..];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias.
|
||||
/// </summary>
|
||||
/// <param name="alias">The text to filter.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <returns>The safe alias.</returns>
|
||||
public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, string culture) =>
|
||||
shortStringHelper.CleanStringForSafeAlias(alias, culture);
|
||||
|
||||
// the new methods to get a url segment
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string to produce a string that can safely be used in an url segment.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to filter.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <returns>The safe url segment.</returns>
|
||||
public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Value can't be empty or consist only of white-space characters.",
|
||||
nameof(text));
|
||||
}
|
||||
|
||||
return shortStringHelper.CleanStringForUrlSegment(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url
|
||||
/// segment.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to filter.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The safe url segment.</returns>
|
||||
public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper, string? culture)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Value can't be empty or consist only of white-space characters.",
|
||||
nameof(text));
|
||||
}
|
||||
|
||||
return shortStringHelper.CleanStringForUrlSegment(text, culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to clean.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <param name="stringType">
|
||||
/// A flag indicating the target casing and encoding of the string. By default,
|
||||
/// strings are cleaned up to camelCase and Ascii.
|
||||
/// </param>
|
||||
/// <returns>The clean string.</returns>
|
||||
/// <remarks>The string is cleaned in the context of the ICurrent.ShortStringHelper default culture.</remarks>
|
||||
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType) => shortStringHelper.CleanString(text, stringType);
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string, using a specified separator.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to clean.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <param name="stringType">
|
||||
/// A flag indicating the target casing and encoding of the string. By default,
|
||||
/// strings are cleaned up to camelCase and Ascii.
|
||||
/// </param>
|
||||
/// <param name="separator">The separator.</param>
|
||||
/// <returns>The clean string.</returns>
|
||||
/// <remarks>The string is cleaned in the context of the ICurrent.ShortStringHelper default culture.</remarks>
|
||||
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator) => shortStringHelper.CleanString(text, stringType, separator);
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string in the context of a specified culture.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to clean.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <param name="stringType">
|
||||
/// A flag indicating the target casing and encoding of the string. By default,
|
||||
/// strings are cleaned up to camelCase and Ascii.
|
||||
/// </param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The clean string.</returns>
|
||||
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, string culture) => shortStringHelper.CleanString(text, stringType, culture);
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string in the context of a specified culture, using a specified separator.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to clean.</param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <param name="stringType">
|
||||
/// A flag indicating the target casing and encoding of the string. By default,
|
||||
/// strings are cleaned up to camelCase and Ascii.
|
||||
/// </param>
|
||||
/// <param name="separator">The separator.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The clean string.</returns>
|
||||
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator, string culture) =>
|
||||
shortStringHelper.CleanString(text, stringType, separator, culture);
|
||||
|
||||
// note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing.
|
||||
// other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results.
|
||||
|
||||
/// <summary>
|
||||
/// Splits a Pascal cased string into a phrase separated by spaces.
|
||||
/// </summary>
|
||||
/// <param name="phrase">The text to split.</param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
/// <returns>The split text.</returns>
|
||||
public static string SplitPascalCasing(this string phrase, IShortStringHelper shortStringHelper) =>
|
||||
shortStringHelper.SplitPascalCasing(phrase, ' ');
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a
|
||||
/// filename,
|
||||
/// both internally (on disk) and externally (as a url).
|
||||
/// </summary>
|
||||
/// <param name="text">The text to filter.</param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
/// <returns>The safe filename.</returns>
|
||||
public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) =>
|
||||
shortStringHelper.CleanStringForSafeFileName(text);
|
||||
|
||||
// NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete.
|
||||
// it basically is yet another version of SplitPascalCasing
|
||||
// plugging string extensions here to be 99% compatible
|
||||
// the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too,
|
||||
// but the legacy one does "Number6Is"... assuming it is not a big deal.
|
||||
internal static string SpaceCamelCasing(this string phrase, IShortStringHelper shortStringHelper) =>
|
||||
phrase.Length < 2 ? phrase : phrase.SplitPascalCasing(shortStringHelper).ToFirstUpperInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a
|
||||
/// filename,
|
||||
/// both internally (on disk) and externally (as a url).
|
||||
/// </summary>
|
||||
/// <param name="text">The text to filter.</param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The safe filename.</returns>
|
||||
public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper, string culture) =>
|
||||
shortStringHelper.CleanStringForSafeFileName(text, culture);
|
||||
}
|
||||
258
src/Umbraco.Core/Extensions/StringExtensions.Parsing.cs
Normal file
258
src/Umbraco.Core/Extensions/StringExtensions.Parsing.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Parsing, detection, and splitting extensions.
|
||||
/// </summary>
|
||||
public static partial class StringExtensions
|
||||
{
|
||||
internal static readonly string[] JsonEmpties = { "[]", "{}" };
|
||||
private const char DefaultEscapedStringEscapeChar = '\\';
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether a specified string is null, empty, or
|
||||
/// consists only of white-space characters.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check.</param>
|
||||
/// <returns>
|
||||
/// Returns <see langword="true" /> if the value is null,
|
||||
/// empty, or consists only of white-space characters, otherwise
|
||||
/// returns <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
|
||||
|
||||
[return: NotNullIfNotNull("defaultValue")]
|
||||
public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) =>
|
||||
str.IsNullOrWhiteSpace() ? defaultValue : str;
|
||||
|
||||
[return: NotNullIfNotNull(nameof(alternative))]
|
||||
public static string? OrIfNullOrWhiteSpace(this string? input, string? alternative) =>
|
||||
!string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: alternative;
|
||||
|
||||
/// <summary>
|
||||
/// Turns an null-or-whitespace string into a null string.
|
||||
/// </summary>
|
||||
public static string? NullOrWhiteSpaceAsNull(this string? text)
|
||||
=> string.IsNullOrWhiteSpace(text) ? null : text;
|
||||
|
||||
/// <summary>
|
||||
/// This tries to detect a json string, this is not a fail safe way but it is quicker than doing
|
||||
/// a try/catch when deserializing when it is not json.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static bool DetectIsJson(this string input)
|
||||
{
|
||||
if (input.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
input = input.Trim();
|
||||
return (input[0] is '[' && input[^1] is ']') || (input[0] is '{' && input[^1] is '}');
|
||||
}
|
||||
|
||||
public static bool DetectIsEmptyJson(this string input) =>
|
||||
JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty));
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="val"></param>
|
||||
/// <returns></returns>
|
||||
public static T? ParseInto<T>(this string val) => (T?)val.ParseInto(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method
|
||||
/// </summary>
|
||||
/// <param name="val"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public static object? ParseInto(this string val, Type type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(val) == false)
|
||||
{
|
||||
TypeConverter tc = TypeDescriptor.GetConverter(type);
|
||||
return tc.ConvertFrom(val);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>enum try parse.</summary>
|
||||
/// <param name="strType">The str type.</param>
|
||||
/// <param name="ignoreCase">The ignore case.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <typeparam name="T">The type</typeparam>
|
||||
/// <returns>The enum try parse.</returns>
|
||||
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")]
|
||||
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")]
|
||||
public static bool EnumTryParse<T>(this string strType, bool ignoreCase, out T? result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = (T)Enum.Parse(typeof(T), strType, ignoreCase);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse string to Enum
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type</typeparam>
|
||||
/// <param name="strType">The string to parse</param>
|
||||
/// <param name="ignoreCase">The ignore case</param>
|
||||
/// <returns>The parsed enum</returns>
|
||||
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")]
|
||||
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")]
|
||||
public static T EnumParse<T>(this string strType, bool ignoreCase) => (T)Enum.Parse(typeof(T), strType, ignoreCase);
|
||||
|
||||
/// <summary>The to delimited list.</summary>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <param name="delimiter">The delimiter.</param>
|
||||
/// <returns>the list</returns>
|
||||
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")]
|
||||
public static IList<string> ToDelimitedList(this string list, string delimiter = ",")
|
||||
{
|
||||
var delimiters = new[] { delimiter };
|
||||
return !list.IsNullOrWhiteSpace()
|
||||
? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList()
|
||||
: new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string with an escape character that allows for the split character to exist in a string
|
||||
/// </summary>
|
||||
/// <param name="value">The string to split</param>
|
||||
/// <param name="splitChar">The character to split on</param>
|
||||
/// <param name="escapeChar">The character which can be used to escape the character to split on</param>
|
||||
/// <returns>The string split into substrings delimited by the split character</returns>
|
||||
public static IEnumerable<string> EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(value.Length);
|
||||
var escaped = false;
|
||||
|
||||
foreach (var chr in value.ToCharArray())
|
||||
{
|
||||
if (escaped)
|
||||
{
|
||||
escaped = false;
|
||||
sb.Append(chr);
|
||||
}
|
||||
else if (chr == splitChar)
|
||||
{
|
||||
yield return sb.ToString();
|
||||
sb.Clear();
|
||||
}
|
||||
else if (chr == escapeChar)
|
||||
{
|
||||
escaped = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(chr);
|
||||
}
|
||||
}
|
||||
|
||||
yield return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns
|
||||
/// true if it does or false if it doesn't
|
||||
/// </summary>
|
||||
/// <param name="haystack">The string to check</param>
|
||||
/// <param name="needles">The collection of strings to check are contained within the first string</param>
|
||||
/// <param name="comparison">
|
||||
/// The type of comparison to perform - defaults to <see cref="StringComparison.CurrentCulture" />
|
||||
/// </param>
|
||||
/// <returns>True if any of the needles are contained with haystack; otherwise returns false</returns>
|
||||
/// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313
|
||||
public static bool ContainsAny(this string haystack, IEnumerable<string> needles, StringComparison comparison = StringComparison.CurrentCulture)
|
||||
{
|
||||
if (haystack == null)
|
||||
{
|
||||
throw new ArgumentNullException("haystack");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return needles.Any(value => haystack.IndexOf(value, comparison) >= 0);
|
||||
}
|
||||
|
||||
public static bool CsvContains(this string csv, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(csv))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return idCheckList.Contains(value);
|
||||
}
|
||||
|
||||
// having benchmarked various solutions (incl. for/foreach, split and LINQ based ones),
|
||||
// this is by far the fastest way to find string needles in a string haystack
|
||||
public static int CountOccurrences(this string haystack, string needle)
|
||||
=> haystack.Length - haystack.Replace(needle, string.Empty).Length;
|
||||
|
||||
/// <summary>
|
||||
/// Convert a path to node ids in the order from right to left (deepest to shallowest).
|
||||
/// </summary>
|
||||
/// <param name="path">The path string expected as a comma delimited collection of integers.</param>
|
||||
/// <returns>An array of integers matching the provided path.</returns>
|
||||
public static int[] GetIdsFromPathReversed(this string path)
|
||||
{
|
||||
ReadOnlySpan<char> pathSpan = path.AsSpan();
|
||||
|
||||
// Using the explicit enumerator (while/MoveNext) over the SpanSplitEnumerator in a foreach loop to avoid any compiler
|
||||
// boxing of the ref struct enumerator.
|
||||
// This prevents potential InvalidProgramException across compilers/JITs ("Cannot create boxed ByRef-like values.").
|
||||
MemoryExtensions.SpanSplitEnumerator<char> pathSegmentsEnumerator = pathSpan.Split(Constants.CharArrays.Comma);
|
||||
|
||||
List<int> nodeIds = [];
|
||||
while (pathSegmentsEnumerator.MoveNext())
|
||||
{
|
||||
Range rangeOfPathSegment = pathSegmentsEnumerator.Current;
|
||||
if (int.TryParse(pathSpan[rangeOfPathSegment], NumberStyles.Integer, CultureInfo.InvariantCulture, out int pathSegment))
|
||||
{
|
||||
nodeIds.Add(pathSegment);
|
||||
}
|
||||
}
|
||||
|
||||
var result = new int[nodeIds.Count];
|
||||
var resultIndex = 0;
|
||||
for (int i = nodeIds.Count - 1; i >= 0; i--)
|
||||
{
|
||||
result[resultIndex++] = nodeIds[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
223
src/Umbraco.Core/Extensions/StringExtensions.Sanitization.cs
Normal file
223
src/Umbraco.Core/Extensions/StringExtensions.Sanitization.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// XSS, HTML, file path, and sanitization extensions.
|
||||
/// </summary>
|
||||
public static partial class StringExtensions
|
||||
{
|
||||
private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray();
|
||||
|
||||
// From: http://stackoverflow.com/a/961504/5018
|
||||
// filters control characters but allows only properly-formed surrogate sequences
|
||||
private static readonly Lazy<Regex> InvalidXmlChars = new(() =>
|
||||
new Regex(
|
||||
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
|
||||
RegexOptions.Compiled));
|
||||
|
||||
/// <summary>
|
||||
/// Cleans string to aid in preventing xss attacks.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="ignoreFromClean"></param>
|
||||
/// <returns></returns>
|
||||
public static string CleanForXss(this string input, params char[] ignoreFromClean)
|
||||
{
|
||||
// remove any HTML
|
||||
input = input.StripHtml();
|
||||
|
||||
// strip out any potential chars involved with XSS
|
||||
return input.ExceptChars(new HashSet<char>(CleanForXssChars.Except(ignoreFromClean)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips all HTML from a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>Returns the string without any HTML tags.</returns>
|
||||
public static string StripHtml(this string text)
|
||||
{
|
||||
const string pattern = @"<(.|\n)*?>";
|
||||
return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An extension method that returns a new string in which all occurrences of an
|
||||
/// unicode characters that are invalid in XML files are replaced with an empty string.
|
||||
/// </summary>
|
||||
/// <param name="text">Current instance of the string</param>
|
||||
/// <returns>Updated string</returns>
|
||||
/// <summary>
|
||||
/// removes any unusual unicode characters that can't be encoded into XML
|
||||
/// </summary>
|
||||
public static string ToValidXmlString(this string text) =>
|
||||
string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, string.Empty);
|
||||
|
||||
public static string EscapeRegexSpecialCharacters(this string text)
|
||||
{
|
||||
var regexSpecialCharacters = new Dictionary<string, string>
|
||||
{
|
||||
{ ".", @"\." },
|
||||
{ "(", @"\(" },
|
||||
{ ")", @"\)" },
|
||||
{ "]", @"\]" },
|
||||
{ "[", @"\[" },
|
||||
{ "{", @"\{" },
|
||||
{ "}", @"\}" },
|
||||
{ "?", @"\?" },
|
||||
{ "!", @"\!" },
|
||||
{ "$", @"\$" },
|
||||
{ "^", @"\^" },
|
||||
{ "+", @"\+" },
|
||||
{ "*", @"\*" },
|
||||
{ "|", @"\|" },
|
||||
{ "<", @"\<" },
|
||||
{ ">", @"\>" },
|
||||
};
|
||||
return ReplaceMany(text, regexSpecialCharacters);
|
||||
}
|
||||
|
||||
public static string StripFileExtension(this string fileName)
|
||||
{
|
||||
// filenames cannot contain line breaks
|
||||
if (fileName.Contains('\n') || fileName.Contains('\r'))
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> spanFileName = fileName.AsSpan();
|
||||
var lastIndex = spanFileName.LastIndexOf('.');
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
ReadOnlySpan<char> ext = spanFileName[lastIndex..];
|
||||
|
||||
// file extensions cannot contain whitespace
|
||||
if (ext.Contains(' '))
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
return new string(spanFileName[..lastIndex]);
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the extension of the path or URL
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns>Extension of the file</returns>
|
||||
public static string GetFileExtension(this string file)
|
||||
{
|
||||
// Find any characters between the last . and the start of a query string or the end of the string
|
||||
const string pattern = @"(?<extension>\.[^\.\?]+)(\?.*|$)";
|
||||
Match match = Regex.Match(file, pattern);
|
||||
return match.Success
|
||||
? match.Groups["extension"].Value
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the folder path ends with a DirectorySeparatorChar
|
||||
/// </summary>
|
||||
/// <param name="currentFolder"></param>
|
||||
/// <returns></returns>
|
||||
public static string NormaliseDirectoryPath(this string currentFolder)
|
||||
{
|
||||
currentFolder = currentFolder
|
||||
.IfNull(x => string.Empty)
|
||||
.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
return currentFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given path is a full path including drive letter
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsFullPath(this string path) => Path.IsPathFullyQualified(path);
|
||||
|
||||
/// <summary>
|
||||
/// This will append the query string to the URL
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="queryStrings"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are
|
||||
/// delimited properly with '&'
|
||||
/// </remarks>
|
||||
public static string AppendQueryStringToUrl(this string url, params string[] queryStrings)
|
||||
{
|
||||
// remove any prefixed '&' or '?'
|
||||
for (var i = 0; i < queryStrings.Length; i++)
|
||||
{
|
||||
queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand)
|
||||
.TrimEnd(Constants.CharArrays.Ampersand);
|
||||
}
|
||||
|
||||
var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
|
||||
|
||||
if (url.Contains('?'))
|
||||
{
|
||||
return url + string.Join("&", nonEmpty).EnsureStartsWith('&');
|
||||
}
|
||||
|
||||
return url + string.Join("&", nonEmpty).EnsureStartsWith('?');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a file name to a friendly name for a content item
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFriendlyName(this string fileName)
|
||||
{
|
||||
// strip the file extension
|
||||
fileName = fileName.StripFileExtension();
|
||||
|
||||
// underscores and dashes to spaces
|
||||
fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' ');
|
||||
|
||||
// any other conversions ?
|
||||
|
||||
// Pascalcase (to be done last)
|
||||
fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName);
|
||||
|
||||
// Replace multiple consecutive spaces with a single space
|
||||
fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a string is a valid email address.
|
||||
/// </summary>
|
||||
/// <param name="email">The string check</param>
|
||||
/// <returns>Returns a bool indicating whether the string is an email address.</returns>
|
||||
public static bool IsEmail(this string? email) =>
|
||||
string.IsNullOrWhiteSpace(email) is false && new EmailAddressAttribute().IsValid(email);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a stream from a string
|
||||
/// </summary>
|
||||
/// <param name="s"></param>
|
||||
/// <returns></returns>
|
||||
internal static Stream GenerateStreamFromString(this string s)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write(s);
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user