using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Web.Security; using Newtonsoft.Json; using Umbraco.Core.Configuration; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Strings; namespace Umbraco.Core { /// /// String extension methods /// public static class StringExtensions { private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); private static readonly char[] ToCSharpEscapeChars; 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]; } /// /// Convert a path to node ids in the order from right to left (deepest to shallowest) /// /// /// internal static int[] GetIdsFromPathReversed(this string path) { var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.TryConvertTo()) .Where(x => x.Success) .Select(x => x.Result) .Reverse() .ToArray(); return nodeIds; } /// /// Removes new lines and tabs /// /// /// internal static string StripWhitespace(this string txt) { return Regex.Replace(txt, @"\s", string.Empty); } internal static string StripFileExtension(this string fileName) { //filenames cannot contain line breaks if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) return fileName; var lastIndex = fileName.LastIndexOf('.'); if (lastIndex > 0) { var ext = fileName.Substring(lastIndex); //file extensions cannot contain whitespace if (ext.Contains(" ")) return fileName; return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal))); } return fileName; } /// /// Based on the input string, this will detect if the string is a JS path or a JS snippet. /// If a path cannot be determined, then it is assumed to be a snippet the original text is returned /// with an invalid attempt, otherwise a valid attempt is returned with the resolved path /// /// /// /// /// This is only used for legacy purposes for the Action.JsSource stuff and shouldn't be needed in v8 /// internal static Attempt DetectIsJavaScriptPath(this string input) { //validate that this is a url, if it is not, we'll assume that it is a text block and render it as a text //block instead. var isValid = true; if (Uri.IsWellFormedUriString(input, UriKind.RelativeOrAbsolute)) { //ok it validates, but so does alert('hello'); ! so we need to do more checks //here are the valid chars in a url without escaping if (Regex.IsMatch(input, @"[^a-zA-Z0-9-._~:/?#\[\]@!$&'\(\)*\+,%;=]")) isValid = false; //we'll have to be smarter and just check for certain js patterns now too! var jsPatterns = new[] { @"\+\s*\=", @"\);", @"function\s*\(", @"!=", @"==" }; if (jsPatterns.Any(p => Regex.IsMatch(input, p))) isValid = false; if (isValid) { var resolvedUrlResult = IOHelper.TryResolveUrl(input); //if the resolution was success, return it, otherwise just return the path, we've detected // it's a path but maybe it's relative and resolution has failed, etc... in which case we're just // returning what was given to us. return resolvedUrlResult.Success ? resolvedUrlResult : Attempt.Succeed(input); } } return Attempt.Fail(input); } /// /// 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. /// /// /// public static bool DetectIsJson(this string input) { if (input.IsNullOrWhiteSpace()) return false; input = input.Trim(); return (input.StartsWith("{") && input.EndsWith("}")) || (input.StartsWith("[") && input.EndsWith("]")); } internal static readonly Lazy Whitespace = new Lazy(() => new Regex(@"\s+", RegexOptions.Compiled)); internal static readonly string[] JsonEmpties = { "[]", "{}" }; internal static bool DetectIsEmptyJson(this string input) { return JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); } /// /// Returns a JObject/JArray instance if the string can be converted to json, otherwise returns the string /// /// /// internal static object ConvertToJsonIfPossible(this string input) { if (input.DetectIsJson() == false) { return input; } try { var obj = JsonConvert.DeserializeObject(input); return obj; } catch (Exception) { return input; } } internal 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; } internal static string ReplaceNonAlphanumericChars(this string input, char replacement) { var inputArray = input.ToCharArray(); var outputArray = new char[input.Length]; for (var i = 0; i < inputArray.Length; i++) outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; return new string(outputArray); } private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); /// /// Cleans string to aid in preventing xss attacks. /// /// /// /// 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(CleanForXssChars.Except(ignoreFromClean))); } public static string ExceptChars(this string str, HashSet toExclude) { var sb = new StringBuilder(str.Length); foreach (var c in str.Where(c => toExclude.Contains(c) == false)) { sb.Append(c); } return sb.ToString(); } /// /// Returns a stream from a string /// /// /// 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; } /// /// This will append the query string to the URL /// /// /// /// /// /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are /// delimited properly with '&' /// internal 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('?', '&').TrimEnd('&'); } var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); if (url.Contains("?")) { return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); } return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); } /// /// Encrypt the string using the MachineKey in medium trust /// /// The string value to be encrypted. /// The encrypted string. public static string EncryptWithMachineKey(this string value) { if (value == null) return null; string valueToEncrypt = value; List parts = new List(); const int EncrpytBlockSize = 500; while (valueToEncrypt.Length > EncrpytBlockSize) { parts.Add(valueToEncrypt.Substring(0, EncrpytBlockSize)); valueToEncrypt = valueToEncrypt.Remove(0, EncrpytBlockSize); } if (valueToEncrypt.Length > 0) { parts.Add(valueToEncrypt); } StringBuilder encrpytedValue = new StringBuilder(); foreach (var part in parts) { var encrpytedBlock = FormsAuthentication.Encrypt(new FormsAuthenticationTicket(0, string.Empty, DateTime.Now, DateTime.MaxValue, false, part)); encrpytedValue.AppendLine(encrpytedBlock); } return encrpytedValue.ToString().TrimEnd(); } /// /// Decrypt the encrypted string using the Machine key in medium trust /// /// The string value to be decrypted /// The decrypted string. public static string DecryptWithMachineKey(this string value) { if (value == null) return null; string[] parts = value.Split('\n'); StringBuilder decryptedValue = new StringBuilder(); foreach (var part in parts) { decryptedValue.Append(FormsAuthentication.Decrypt(part.TrimEnd()).UserData); } return decryptedValue.ToString(); } //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 = name + "es"; return name; } if ((name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && (name.Length > 1)) && !IsVowel(name[name.Length - 2])) { name = name.Remove(name.Length - 1, 1); name = name + "ies"; return name; } if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { name = 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; } /// /// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts char or char[]. /// /// The value. /// For removing. /// public static string Trim(this string value, string forRemoving) { if (string.IsNullOrEmpty(value)) return value; return value.TrimEnd(forRemoving).TrimStart(forRemoving); } 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 = (int)c; if (i < 32 || i > 127) { sb.AppendFormat("\\u{0:X04}", i); } else { sb.Append(c); } break; } } return sb.ToString(); } 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.Substring(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) { return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; } public static string EnsureEndsWith(this string input, char value) { return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; } public static string EnsureEndsWith(this string input, string toEndWith) { return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; } public static bool IsLowerCase(this char ch) { return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); } public static bool IsUpperCase(this char ch) { return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); } /// Indicates whether a specified string is null, empty, or /// consists only of white-space characters. /// The value to check. /// Returns if the value is null, /// empty, or consists only of white-space characters, otherwise /// returns . public static bool IsNullOrWhiteSpace(this string value) => string.IsNullOrWhiteSpace(value); public static string IfNullOrWhiteSpace(this string str, string defaultValue) { return str.IsNullOrWhiteSpace() ? defaultValue : str; } /// The to delimited list. /// The list. /// The delimiter. /// the list [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")] public static IList ToDelimitedList(this string list, string delimiter = ",") { var delimiters = new[] { delimiter }; return !list.IsNullOrWhiteSpace() ? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .ToList() : new List(); } /// enum try parse. /// The str type. /// The ignore case. /// The result. /// The type /// The enum try parse. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] public static bool EnumTryParse(this string strType, bool ignoreCase, out T result) { try { result = (T)Enum.Parse(typeof(T), strType, ignoreCase); return true; } catch { result = default(T); return false; } } /// /// Parse string to Enum /// /// The enum type /// The string to parse /// The ignore case /// The parsed enum [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] public static T EnumParse(this string strType, bool ignoreCase) { return (T)Enum.Parse(typeof(T), strType, ignoreCase); } /// /// Strips all HTML from a string. /// /// The text. /// Returns the string without any HTML tags. public static string StripHtml(this string text) { const string pattern = @"<(.|\n)*?>"; return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); } /// /// Encodes as GUID. /// /// The input. /// 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.Substring(0, hexLength).PadLeft(32, '0'); var output = Guid.Empty; return Guid.TryParse(hex, out output) ? output : Guid.Empty; } /// /// Converts to hex. /// /// The input. /// 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 = ""; while (hexValue.Length > 0) { strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); hexValue = hexValue.Substring(2, hexValue.Length - 2); } return strValue; } /// /// Encodes a string to a safe URL base64 string /// /// /// 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); } /// /// Decodes a URL safe base64 string back /// /// /// 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; } } /// /// formats the string with invariant culture /// /// The format. /// The args. /// public static string InvariantFormat(this string format, params object[] args) { return String.Format(CultureInfo.InvariantCulture, format, args); } /// /// Converts an integer to an invariant formatted string /// /// /// public static string ToInvariantString(this int str) { return str.ToString(CultureInfo.InvariantCulture); } public static string ToInvariantString(this long str) { return str.ToString(CultureInfo.InvariantCulture); } /// /// Compares 2 strings with invariant culture and case ignored /// /// The compare. /// The compare to. /// public static bool InvariantEquals(this string compare, string compareTo) { return String.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); } public static bool InvariantStartsWith(this string compare, string compareTo) { return compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); } public static bool InvariantEndsWith(this string compare, string compareTo) { return compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); } public static bool InvariantContains(this string compare, string compareTo) { return compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; } public static bool InvariantContains(this IEnumerable compare, string compareTo) { return compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); } public static int InvariantIndexOf(this string s, string value) { return s.IndexOf(value, StringComparison.OrdinalIgnoreCase); } public static int InvariantLastIndexOf(this string s, string value) { return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); } /// /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method /// /// /// /// public static T ParseInto(this string val) { return (T)val.ParseInto(typeof(T)); } /// /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method /// /// /// /// 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; } /// /// Generates a hash of a string based on the FIPS compliance setting. /// /// Refers to itself /// The hashed string public static string GenerateHash(this string str) { return CryptoConfig.AllowOnlyFipsAlgorithms ? str.ToSHA1() : str.ToMd5(); } /// /// Converts the string to MD5 /// /// Refers to itself /// The MD5 hashed string [Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")] internal static string ToMd5(this string stringToConvert) { return stringToConvert.GenerateHash("MD5"); } /// /// Converts the string to SHA1 /// /// refers to itself /// The SHA1 hashed string [Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")] internal static string ToSHA1(this string stringToConvert) { return stringToConvert.GenerateHash("SHA1"); } /// Generate a hash of a string based on the hashType passed in /// /// Refers to itself /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values. /// The hashed string private static string GenerateHash(this string str, string hashType) { //create an instance of the correct hashing provider based on the type passed in var 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(); } } /// /// Decodes a string that was encoded with UrlTokenEncode /// /// /// internal static byte[] UrlTokenDecode(string input) { if (input == null) throw new ArgumentNullException(nameof(input)); if (input.Length == 0) return Array.Empty(); // 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); } /// /// Encodes a string so that it is 'safe' for URLs, files, etc.. /// /// /// internal static string UrlTokenEncode(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 var chArray = 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); } /// /// Ensures that the folder path ends with a DirectorySeparatorChar /// /// /// public static string NormaliseDirectoryPath(this string currentFolder) { currentFolder = currentFolder .IfNull(x => String.Empty) .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; return currentFolder; } /// /// Truncates the specified text string. /// /// The text. /// Length of the max. /// The suffix. /// 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.Substring(0, strLength); truncatedString = truncatedString.TrimEnd(); truncatedString += suffix; return truncatedString; } /// /// Strips carrage returns and line feeds from the specified text. /// /// The input. /// public static string StripNewLines(this string input) { return input.Replace("\r", "").Replace("\n", ""); } /// /// Converts to single line by replacing line breaks with spaces. /// 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; } public static string OrIfNullOrWhiteSpace(this string input, string alternative) { return !string.IsNullOrWhiteSpace(input) ? input : alternative; } /// /// Returns a copy of the string with the first character converted to uppercase. /// /// The string. /// The converted string. public static string ToFirstUpper(this string input) { return string.IsNullOrWhiteSpace(input) ? input : input.Substring(0, 1).ToUpper() + input.Substring(1); } /// /// Returns a copy of the string with the first character converted to lowercase. /// /// The string. /// The converted string. public static string ToFirstLower(this string input) { return string.IsNullOrWhiteSpace(input) ? input : input.Substring(0, 1).ToLower() + input.Substring(1); } /// /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the specified culture. /// /// The string. /// The culture. /// The converted string. public static string ToFirstUpper(this string input, CultureInfo culture) { return string.IsNullOrWhiteSpace(input) ? input : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); } /// /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the specified culture. /// /// The string. /// The culture. /// The converted string. public static string ToFirstLower(this string input, CultureInfo culture) { return string.IsNullOrWhiteSpace(input) ? input : input.Substring(0, 1).ToLower(culture) + input.Substring(1); } /// /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the invariant culture. /// /// The string. /// The converted string. public static string ToFirstUpperInvariant(this string input) { return string.IsNullOrWhiteSpace(input) ? input : input.Substring(0, 1).ToUpperInvariant() + input.Substring(1); } /// /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the invariant culture. /// /// The string. /// The converted string. public static string ToFirstLowerInvariant(this string input) { return string.IsNullOrWhiteSpace(input) ? input : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); } /// /// Returns a new string in which all occurrences of specified strings are replaced by other specified strings. /// /// The string to filter. /// The replacements definition. /// The filtered string. public static string ReplaceMany(this string text, IDictionary replacements) { if (text == null) throw new ArgumentNullException(nameof(text)); if (replacements == null) throw new ArgumentNullException(nameof(replacements)); foreach (KeyValuePair item in replacements) text = text.Replace(item.Key, item.Value); return text; } /// /// Returns a new string in which all occurrences of specified characters are replaced by a specified character. /// /// The string to filter. /// The characters to replace. /// The replacement character. /// The filtered string. 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 (int i = 0; i < chars.Length; i++) text = text.Replace(chars[i], replacement); return text; } // FORMAT STRINGS /// /// Cleans a string to produce a string that can safely be used in an alias. /// /// The text to filter. /// The safe alias. public static string ToSafeAlias(this string alias) { return Current.ShortStringHelper.CleanStringForSafeAlias(alias); } /// /// Cleans a string to produce a string that can safely be used in an alias. /// /// The text to filter. /// A value indicating that we want to camel-case the alias. /// The safe alias. public static string ToSafeAlias(this string alias, bool camel) { var a = Current.ShortStringHelper.CleanStringForSafeAlias(alias); if (string.IsNullOrWhiteSpace(a) || camel == false) return a; return char.ToLowerInvariant(a[0]) + a.Substring(1); } /// /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. /// /// The text to filter. /// The culture. /// The safe alias. public static string ToSafeAlias(this string alias, string culture) { return Current.ShortStringHelper.CleanStringForSafeAlias(alias, culture); } // the new methods to get a url segment /// /// Cleans a string to produce a string that can safely be used in an url segment. /// /// The text to filter. /// The safe url segment. public static string ToUrlSegment(this string text) { if (string.IsNullOrWhiteSpace(text)) throw new ArgumentNullOrEmptyException(nameof(text)); return Current.ShortStringHelper.CleanStringForUrlSegment(text); } /// /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url segment. /// /// The text to filter. /// The culture. /// The safe url segment. public static string ToUrlSegment(this string text, string culture) { if (string.IsNullOrWhiteSpace(text)) throw new ArgumentNullOrEmptyException(nameof(text)); return Current.ShortStringHelper.CleanStringForUrlSegment(text, culture); } // the new methods to clean a string (to alias, url segment...) /// /// Cleans a string. /// /// The text to clean. /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The clean string. /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. public static string ToCleanString(this string text, CleanStringType stringType) { return Current.ShortStringHelper.CleanString(text, stringType); } /// /// Cleans a string, using a specified separator. /// /// The text to clean. /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The separator. /// The clean string. /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. public static string ToCleanString(this string text, CleanStringType stringType, char separator) { return Current.ShortStringHelper.CleanString(text, stringType, separator); } /// /// Cleans a string in the context of a specified culture. /// /// The text to clean. /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The culture. /// The clean string. public static string ToCleanString(this string text, CleanStringType stringType, string culture) { return Current.ShortStringHelper.CleanString(text, stringType, culture); } /// /// Cleans a string in the context of a specified culture, using a specified separator. /// /// The text to clean. /// A flag indicating the target casing and encoding of the string. By default, /// strings are cleaned up to camelCase and Ascii. /// The separator. /// The culture. /// The clean string. public static string ToCleanString(this string text, CleanStringType stringType, char separator, string culture) { return Current.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. /// /// Splits a Pascal cased string into a phrase separated by spaces. /// /// The text to split. /// The split text. public static string SplitPascalCasing(this string phrase) { return Current.ShortStringHelper.SplitPascalCasing(phrase, ' '); } //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) { return phrase.Length < 2 ? phrase : phrase.SplitPascalCasing().ToFirstUpperInvariant(); } /// /// 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). /// /// The text to filter. /// The safe filename. public static string ToSafeFileName(this string text) { return Current.ShortStringHelper.CleanStringForSafeFileName(text); } /// /// 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). /// /// The text to filter. /// The culture. /// The safe filename. public static string ToSafeFileName(this string text, string culture) { return Current.ShortStringHelper.CleanStringForSafeFileName(text, culture); } /// /// 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. /// /// Current instance of the string /// Specified string to replace /// Specified string to inject /// String Comparison object to specify search type /// Updated string 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. int 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; } /// /// Converts a literal string into a C# expression. /// /// Current instance of the string. /// The string in a C# format. public static string ToCSharpString(this string s) { if (s == null) return ""; // 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 && '\0' != ToCSharpEscapeChars[c]) 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 EscapeRegexSpecialCharacters(this string text) { var regexSpecialCharacters = new Dictionary { {".", @"\."}, {"(", @"\("}, {")", @"\)"}, {"]", @"\]"}, {"[", @"\["}, {"{", @"\{"}, {"}", @"\}"}, {"?", @"\?"}, {"!", @"\!"}, {"$", @"\$"}, {"^", @"\^"}, {"+", @"\+"}, {"*", @"\*"}, {"|", @"\|"}, {"<", @"\<"}, {">", @"\>"} }; return ReplaceMany(text, regexSpecialCharacters); } /// /// 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 /// /// The string to check /// The collection of strings to check are contained within the first string /// The type of comparison to perform - defaults to /// True if any of the needles are contained with haystack; otherwise returns false /// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313 public static bool ContainsAny(this string haystack, IEnumerable 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(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); return idCheckList.Contains(value); } /// /// Converts a file name to a friendly name for a content item /// /// /// public static string ToFriendlyName(this string fileName) { // strip the file extension fileName = fileName.StripFileExtension(); // underscores and dashes to spaces fileName = fileName.ReplaceMany(new[] { '_', '-' }, ' '); // 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(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); return fileName; } // From: http://stackoverflow.com/a/961504/5018 // filters control characters but allows only properly-formed surrogate sequences private static readonly Lazy InvalidXmlChars = new Lazy(() => new Regex( @"(? /// 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. /// /// Current instance of the string /// Updated string /// /// /// removes any unusual unicode characters that can't be encoded into XML /// internal static string ToValidXmlString(this string text) { return string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, ""); } /// /// Converts a string to a Guid - WARNING, depending on the string, this may not be unique /// /// /// internal static Guid ToGuid(this string text) { return CreateGuidFromHash(UrlNamespace, text, CryptoConfig.AllowOnlyFipsAlgorithms ? 5 // SHA1 : 3); // MD5 } /// /// The namespace for URLs (from RFC 4122, Appendix C). /// /// See RFC 4122 /// internal static readonly Guid UrlNamespace = new Guid("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); /// /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. /// /// See GuidUtility.cs for original implementation. /// /// The ID of the namespace. /// The name (within that namespace). /// The version number of the UUID to create; this value must be either /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). /// A UUID derived from the namespace and name. /// See Generating a deterministic GUID. 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 byte[] nameBytes = Encoding.UTF8.GetBytes(name); // convert the namespace UUID to network order (step 3) byte[] 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 ? (HashAlgorithm)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) byte[] newGuid = new byte[16]; Array.Copy(hash, 0, newGuid, 0, 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(byte[] guid) { SwapBytes(guid, 0, 3); SwapBytes(guid, 1, 2); SwapBytes(guid, 4, 5); SwapBytes(guid, 6, 7); } private static void SwapBytes(byte[] guid, int left, int right) { byte temp = guid[left]; guid[left] = guid[right]; guid[right] = temp; } /// /// Turns an null-or-whitespace string into a null string. /// public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; } }