diff --git a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs b/src/Umbraco.Abstractions/Collections/CompositeTypeTypeKey.cs similarity index 95% rename from src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs rename to src/Umbraco.Abstractions/Collections/CompositeTypeTypeKey.cs index cc555afe55..e08e3305d2 100644 --- a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs +++ b/src/Umbraco.Abstractions/Collections/CompositeTypeTypeKey.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Collections /// /// Represents a composite key of (Type, Type) for fast dictionaries. /// - internal struct CompositeTypeTypeKey : IEquatable + public struct CompositeTypeTypeKey : IEquatable { /// /// Initializes a new instance of the struct. diff --git a/src/Umbraco.Core/CustomBooleanTypeConverter.cs b/src/Umbraco.Abstractions/CustomBooleanTypeConverter.cs similarity index 100% rename from src/Umbraco.Core/CustomBooleanTypeConverter.cs rename to src/Umbraco.Abstractions/CustomBooleanTypeConverter.cs diff --git a/src/Umbraco.Core/ExpressionHelper.cs b/src/Umbraco.Abstractions/ExpressionHelper.cs similarity index 98% rename from src/Umbraco.Core/ExpressionHelper.cs rename to src/Umbraco.Abstractions/ExpressionHelper.cs index 7d5a246f0d..93598fe97e 100644 --- a/src/Umbraco.Core/ExpressionHelper.cs +++ b/src/Umbraco.Abstractions/ExpressionHelper.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core /// A set of helper methods for dealing with expressions /// /// - internal static class ExpressionHelper + public static class ExpressionHelper { private static readonly ConcurrentDictionary PropertyInfoCache = new ConcurrentDictionary(); @@ -106,7 +106,7 @@ namespace Umbraco.Core case ExpressionType.Call: var callExpr = (MethodCallExpression) expr; var method = callExpr.Method; - if (method.DeclaringType != typeof(NPocoSqlExtensions.Statics) || method.Name != "Alias" || !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) + if (method.DeclaringType != typeof(SqlExtensionsStatics) || method.Name != "Alias" || !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) Throw(); expr = callExpr.Arguments[0]; alias = aliasExpr.Value.ToString(); diff --git a/src/Umbraco.Core/LambdaExpressionCacheKey.cs b/src/Umbraco.Abstractions/LambdaExpressionCacheKey.cs similarity index 100% rename from src/Umbraco.Core/LambdaExpressionCacheKey.cs rename to src/Umbraco.Abstractions/LambdaExpressionCacheKey.cs diff --git a/src/Umbraco.Abstractions/ObjectExtensions.cs b/src/Umbraco.Abstractions/ObjectExtensions.cs new file mode 100644 index 0000000000..bd4d17b871 --- /dev/null +++ b/src/Umbraco.Abstractions/ObjectExtensions.cs @@ -0,0 +1,761 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml; +using Umbraco.Core.Collections; + +namespace Umbraco.Core +{ + /// + /// Provides object extension methods. + /// + public static class ObjectExtensions + { + + private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary(); + + private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; + private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter(); + + //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); + + /// + /// + /// + /// + /// + /// + public static IEnumerable AsEnumerableOfOne(this T input) + { + return Enumerable.Repeat(input, 1); + } + + /// + /// + /// + /// + public static void DisposeIfDisposable(this object input) + { + if (input is IDisposable disposable) + disposable.Dispose(); + } + + /// + /// Provides a shortcut way of safely casting an input when you cannot guarantee the is + /// an instance type (i.e., when the C# AS keyword is not applicable). + /// + /// + /// The input. + /// + public static T SafeCast(this object input) + { + if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default; + if (input is T variable) return variable; + return default; + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The type to convert to + /// The input. + /// The + public static Attempt TryConvertTo(this object input) + { + var result = TryConvertTo(input, typeof(T)); + + if (result.Success) + return Attempt.Succeed((T)result.Result); + + // just try to cast + try + { + return Attempt.Succeed((T)input); + } + catch (Exception e) + { + return Attempt.Fail(e); + } + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + public static Attempt TryConvertTo(this object input, Type target) + { + if (target == null) + { + return Attempt.Fail(); + } + + try + { + if (input == null) + { + // Nullable is ok + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) + { + return Attempt.Succeed(null); + } + + // Reference types are ok + return Attempt.If(target.IsValueType == false, null); + } + + var inputType = input.GetType(); + + // Easy + if (target == typeof(object) || inputType == target) + { + return Attempt.Succeed(input); + } + + // Check for string so that overloaders of ToString() can take advantage of the conversion. + if (target == typeof(string)) + { + return Attempt.Succeed(input.ToString()); + } + + // If we've got a nullable of something, we try to convert directly to that thing. + // We cache the destination type and underlying nullable types + // Any other generic types need to fall through + if (target.IsGenericType) + { + var underlying = GetCachedGenericNullableType(target); + if (underlying != null) + { + // Special case for empty strings for bools/dates which should return null if an empty string. + if (input is string inputString) + { + // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? + if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool))) + { + return Attempt.Succeed(null); + } + } + + // Recursively call into this method with the inner (not-nullable) type and handle the outcome + var inner = input.TryConvertTo(underlying); + + // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception + if (inner.Success) + { + input = inner.Result; // Now fall on through... + } + else + { + return Attempt.Fail(inner.Exception); + } + } + } + else + { + // target is not a generic type + + if (input is string inputString) + { + // Try convert from string, returns an Attempt if the string could be + // processed (either succeeded or failed), else null if we need to try + // other methods + var result = TryConvertToFromString(inputString, target); + if (result.HasValue) + { + return result.Value; + } + } + + // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with + // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. + if (GetCachedCanAssign(input, inputType, target)) + { + return Attempt.Succeed(Convert.ChangeType(input, target)); + } + } + + if (target == typeof(bool)) + { + if (GetCachedCanConvertToBoolean(inputType)) + { + return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input)); + } + } + + var inputConverter = GetCachedSourceTypeConverter(inputType, target); + if (inputConverter != null) + { + return Attempt.Succeed(inputConverter.ConvertTo(input, target)); + } + + var outputConverter = GetCachedTargetTypeConverter(inputType, target); + if (outputConverter != null) + { + return Attempt.Succeed(outputConverter.ConvertFrom(input)); + } + + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) + { + // cannot Convert.ChangeType as that does not work with nullable + // input has already been converted to the underlying type - just + // return input, there's an implicit conversion from T to T? anyways + return Attempt.Succeed(input); + } + + // Re-check convertibles since we altered the input through recursion + if (input is IConvertible convertible2) + { + return Attempt.Succeed(Convert.ChangeType(convertible2, target)); + } + } + catch (Exception e) + { + return Attempt.Fail(e); + } + + return Attempt.Fail(); + } + + /// + /// Attempts to convert the input string to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + private static Attempt? TryConvertToFromString(this string input, Type target) + { + // Easy + if (target == typeof(string)) + { + return Attempt.Succeed(input); + } + + // Null, empty, whitespaces + if (string.IsNullOrWhiteSpace(input)) + { + if (target == typeof(bool)) + { + // null/empty = bool false + return Attempt.Succeed(false); + } + + if (target == typeof(DateTime)) + { + // null/empty = min DateTime value + return Attempt.Succeed(DateTime.MinValue); + } + + // Cannot decide here, + // Any of the types below will fail parsing and will return a failed attempt + // but anything else will not be processed and will return null + // so even though the string is null/empty we have to proceed. + } + + // Look for type conversions in the expected order of frequency of use. + // + // By using a mixture of ordered if statements and switches we can optimize both for + // fast conditional checking for most frequently used types and the branching + // that does not depend on previous values available to switch statements. + if (target.IsPrimitive) + { + if (target == typeof(int)) + { + if (int.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Because decimal 100.01m will happily convert to integer 100, it + // makes sense that string "100.01" *also* converts to integer 100. + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); + } + + if (target == typeof(long)) + { + if (long.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Same as int + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); + } + + // TODO: Should we do the decimal trick for short, byte, unsigned? + + if (target == typeof(bool)) + { + if (bool.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Don't declare failure so the CustomBooleanTypeConverter can try + return null; + } + + // Calling this method directly is faster than any attempt to cache it. + switch (Type.GetTypeCode(target)) + { + case TypeCode.Int16: + return Attempt.If(short.TryParse(input, out var value), value); + + case TypeCode.Double: + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(double.TryParse(input2, out var valueD), valueD); + + case TypeCode.Single: + var input3 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(float.TryParse(input3, out var valueF), valueF); + + case TypeCode.Char: + return Attempt.If(char.TryParse(input, out var valueC), valueC); + + case TypeCode.Byte: + return Attempt.If(byte.TryParse(input, out var valueB), valueB); + + case TypeCode.SByte: + return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); + + case TypeCode.UInt32: + return Attempt.If(uint.TryParse(input, out var valueU), valueU); + + case TypeCode.UInt16: + return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); + + case TypeCode.UInt64: + return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); + } + } + else if (target == typeof(Guid)) + { + return Attempt.If(Guid.TryParse(input, out var value), value); + } + else if (target == typeof(DateTime)) + { + if (DateTime.TryParse(input, out var value)) + { + switch (value.Kind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return Attempt.Succeed(value); + + case DateTimeKind.Local: + return Attempt.Succeed(value.ToUniversalTime()); + + default: + throw new ArgumentOutOfRangeException(); + } + } + + return Attempt.Fail(); + } + else if (target == typeof(DateTimeOffset)) + { + return Attempt.If(DateTimeOffset.TryParse(input, out var value), value); + } + else if (target == typeof(TimeSpan)) + { + return Attempt.If(TimeSpan.TryParse(input, out var value), value); + } + else if (target == typeof(decimal)) + { + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value), value); + } + else if (input != null && target == typeof(Version)) + { + return Attempt.If(Version.TryParse(input, out var value), value); + } + + // E_NOTIMPL IPAddress, BigInteger + return null; // we can't decide... + } + internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) + { + // TODO: Localize this exception + if (isDisposed) + throw new ObjectDisposedException(objectname); + } + + //public enum PropertyNamesCaseType + //{ + // CamelCase, + // CaseInsensitive + //} + + ///// + ///// Convert an object to a JSON string with camelCase formatting + ///// + ///// + ///// + //public static string ToJsonString(this object obj) + //{ + // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); + //} + + ///// + ///// Convert an object to a JSON string with the specified formatting + ///// + ///// The obj. + ///// Type of the property names case. + ///// + //public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) + //{ + // var type = obj.GetType(); + // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; + + // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) + // { + // return obj.ToString(); + // } + + // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) + // { + // return Convert.ToDateTime(obj).ToString(dateTimeStyle); + // } + + // var serializer = new JsonSerializer(); + + // switch (propertyNamesCaseType) + // { + // case PropertyNamesCaseType.CamelCase: + // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); + // break; + // } + + // var dateTimeConverter = new IsoDateTimeConverter + // { + // DateTimeStyles = System.Globalization.DateTimeStyles.None, + // DateTimeFormat = dateTimeStyle + // }; + + // if (typeof(IDictionary).IsAssignableFrom(type)) + // { + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) + // { + // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + //} + + /// + /// Converts an object into a dictionary + /// + /// + /// + /// + /// + /// + /// + public static IDictionary ToDictionary(this T o, params Expression>[] ignoreProperties) + { + return o.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); + } + + /// + /// Turns object into dictionary + /// + /// + /// Properties to ignore + /// + public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + { + if (o != null) + { + var props = TypeDescriptor.GetProperties(o); + var d = new Dictionary(); + foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) + { + var val = prop.GetValue(o); + if (val != null) + { + d.Add(prop.Name, (TVal)val); + } + } + return d; + } + return new Dictionary(); + } + + + + internal static string ToDebugString(this object obj, int levels = 0) + { + if (obj == null) return "{null}"; + try + { + if (obj is string) + { + return "\"{0}\"".InvariantFormat(obj); + } + if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || obj is int? || obj is short? || obj is long? || obj is float? || obj is double? || obj is bool?) + { + return "{0}".InvariantFormat(obj); + } + if (obj is Enum) + { + return "[{0}]".InvariantFormat(obj); + } + if (obj is IEnumerable) + { + var enumerable = (obj as IEnumerable); + + var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); + + return items.Any() + ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) + : null; + } + + var props = obj.GetType().GetProperties(); + if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) + { + try + { + var key = props[0].GetValue(obj, null) as string; + var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); + return "{0}={1}".InvariantFormat(key, value); + } + catch (Exception) + { + return "[KeyValuePropertyException]"; + } + } + if (levels > -1) + { + var items = + (from propertyInfo in props + let value = GetPropertyDebugString(propertyInfo, obj, levels) + where value != null + select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); + + return items.Any() + ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) + : null; + } + } + catch (Exception ex) + { + return "[Exception:{0}]".InvariantFormat(ex.Message); + } + return null; + } + + /// + /// Attempts to serialize the value to an XmlString using ToXmlString + /// + /// + /// + /// + internal static Attempt TryConvertToXmlString(this object value, Type type) + { + try + { + var output = value.ToXmlString(type); + return Attempt.Succeed(output); + } + catch (NotSupportedException ex) + { + return Attempt.Fail(ex); + } + } + + /// + /// Returns an XmlSerialized safe string representation for the value + /// + /// + /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown + /// + public static string ToXmlString(this object value, Type type) + { + if (value == null) return string.Empty; + if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString()); + if (type == typeof(bool)) return XmlConvert.ToString((bool)value); + if (type == typeof(byte)) return XmlConvert.ToString((byte)value); + if (type == typeof(char)) return XmlConvert.ToString((char)value); + if (type == typeof(DateTime)) return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); + if (type == typeof(DateTimeOffset)) return XmlConvert.ToString((DateTimeOffset)value); + if (type == typeof(decimal)) return XmlConvert.ToString((decimal)value); + if (type == typeof(double)) return XmlConvert.ToString((double)value); + if (type == typeof(float)) return XmlConvert.ToString((float)value); + if (type == typeof(Guid)) return XmlConvert.ToString((Guid)value); + if (type == typeof(int)) return XmlConvert.ToString((int)value); + if (type == typeof(long)) return XmlConvert.ToString((long)value); + if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value); + if (type == typeof(short)) return XmlConvert.ToString((short)value); + if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value); + if (type == typeof(bool)) return XmlConvert.ToString((bool)value); + if (type == typeof(uint)) return XmlConvert.ToString((uint)value); + if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value); + if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value); + + throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert"); + } + + /// + /// Returns an XmlSerialized safe string representation for the value and type + /// + /// + /// + /// + public static string ToXmlString(this object value) + { + return value.ToXmlString(typeof (T)); + } + + private static string GetEnumPropertyDebugString(object enumItem, int levels) + { + try + { + return enumItem.ToDebugString(levels - 1); + } + catch (Exception) + { + return "[GetEnumPartException]"; + } + } + + private static string GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + { + try + { + return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); + } + catch (Exception) + { + return "[GetPropertyValueException]"; + } + } + + public static Guid AsGuid(this object value) + { + return value is Guid guid ? guid : Guid.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } + + // gets a converter for source, that can convert to target, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter GetCachedSourceTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (InputTypeConverterCache.TryGetValue(key, out var typeConverter)) + { + return typeConverter; + } + + var converter = TypeDescriptor.GetConverter(source); + if (converter.CanConvertTo(target)) + { + return InputTypeConverterCache[key] = converter; + } + + return InputTypeConverterCache[key] = null; + } + + // gets a converter for target, that can convert from source, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter GetCachedTargetTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter)) + { + return typeConverter; + } + + var converter = TypeDescriptor.GetConverter(target); + if (converter.CanConvertFrom(source)) + { + return DestinationTypeConverterCache[key] = converter; + } + + return DestinationTypeConverterCache[key] = null; + } + + // gets the underlying type of a nullable type, or null if the type is not nullable + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Type GetCachedGenericNullableType(Type type) + { + if (NullableGenericCache.TryGetValue(type, out var underlyingType)) + { + return underlyingType; + } + + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + Type underlying = Nullable.GetUnderlyingType(type); + return NullableGenericCache[type] = underlying; + } + + return NullableGenericCache[type] = null; + } + + // gets an IConvertible from source to target type, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanAssign(object input, Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + if (AssignableTypeCache.TryGetValue(key, out var canConvert)) + { + return canConvert; + } + + // "object is" is faster than "Type.IsAssignableFrom. + // We can use it to very quickly determine whether true/false + if (input is IConvertible && target.IsAssignableFrom(source)) + { + return AssignableTypeCache[key] = true; + } + + return AssignableTypeCache[key] = false; + } + + // determines whether a type can be converted to boolean + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanConvertToBoolean(Type type) + { + if (BoolConvertCache.TryGetValue(type, out var result)) + { + return result; + } + + if (CustomBooleanTypeConverter.CanConvertFrom(type)) + { + return BoolConvertCache[type] = true; + } + + return BoolConvertCache[type] = false; + } + + + } +} diff --git a/src/Umbraco.Abstractions/Persistence/SqlExtensionsStatics.cs b/src/Umbraco.Abstractions/Persistence/SqlExtensionsStatics.cs new file mode 100644 index 0000000000..0974b8df6d --- /dev/null +++ b/src/Umbraco.Abstractions/Persistence/SqlExtensionsStatics.cs @@ -0,0 +1,45 @@ +using System; + +namespace Umbraco.Core.Persistence +{ + /// + /// Provides a mean to express aliases in SELECT Sql statements. + /// + /// + /// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, + /// then use eg Sql{Foo}(x => Alias(x.Id, "id")). + /// + public static class SqlExtensionsStatics + { + /// + /// Aliases a field. + /// + /// The field to alias. + /// The alias. + public static object Alias(object field, string alias) => field; + + /// + /// Produces Sql text. + /// + /// The name of the field. + /// A function producing Sql text. + public static T SqlText(string field, Func expr) => default; + + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// A function producing Sql text. + public static T SqlText(string field1, string field2, Func expr) => default; + + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// The name of the third field. + /// A function producing Sql text. + public static T SqlText(string field1, string field2, string field3, Func expr) => default; + } +} diff --git a/src/Umbraco.Abstractions/StringExtensions.cs b/src/Umbraco.Abstractions/StringExtensions.cs new file mode 100644 index 0000000000..ce23704872 --- /dev/null +++ b/src/Umbraco.Abstractions/StringExtensions.cs @@ -0,0 +1,1203 @@ +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; + +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) + /// + /// + /// + public 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 + /// + /// + /// + public static string StripWhitespace(this string txt) + { + return Regex.Replace(txt, @"\s", string.Empty); + } + + public 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; + + + } + + + + /// + /// 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 = { "[]", "{}" }; + public static bool DetectIsEmptyJson(this string input) + { + return JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); + } + + + + 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 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('?'); + } + + + + + + //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 + public static string ToMd5(this string stringToConvert) + { + return stringToConvert.GenerateHash("MD5"); + } + + /// + /// Converts the string to SHA1 + /// + /// refers to itself + /// The SHA1 hashed string + public 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 + /// + /// + /// + public static byte[] UrlTokenDecode(this 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.. + /// + /// + /// + 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 + 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; + } + + + + + + /// + /// 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 + /// + public 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 + /// + /// + /// + public 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; + } +} diff --git a/src/Umbraco.Core/Strings/CleanStringType.cs b/src/Umbraco.Abstractions/Strings/CleanStringType.cs similarity index 100% rename from src/Umbraco.Core/Strings/CleanStringType.cs rename to src/Umbraco.Abstractions/Strings/CleanStringType.cs diff --git a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs b/src/Umbraco.Abstractions/Strings/Css/StylesheetHelper.cs similarity index 98% rename from src/Umbraco.Core/Strings/Css/StylesheetHelper.cs rename to src/Umbraco.Abstractions/Strings/Css/StylesheetHelper.cs index 58dddb5712..80315abb95 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs +++ b/src/Umbraco.Abstractions/Strings/Css/StylesheetHelper.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; namespace Umbraco.Core.Strings.Css { - internal class StylesheetHelper + public class StylesheetHelper { private const string RuleRegexFormat = @"/\*\*\s*umb_name:\s*(?{0}?)\s*\*/\s*(?[^,{{]*?)\s*{{\s*(?.*?)\s*}}"; diff --git a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs b/src/Umbraco.Abstractions/Strings/Css/StylesheetRule.cs similarity index 97% rename from src/Umbraco.Core/Strings/Css/StylesheetRule.cs rename to src/Umbraco.Abstractions/Strings/Css/StylesheetRule.cs index 6f91906250..c12cc5b90c 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs +++ b/src/Umbraco.Abstractions/Strings/Css/StylesheetRule.cs @@ -4,7 +4,7 @@ using System.Text; namespace Umbraco.Core.Strings.Css { - internal class StylesheetRule + public class StylesheetRule { public string Name { get; set; } diff --git a/src/Umbraco.Core/Strings/Diff.cs b/src/Umbraco.Abstractions/Strings/Diff.cs similarity index 100% rename from src/Umbraco.Core/Strings/Diff.cs rename to src/Umbraco.Abstractions/Strings/Diff.cs diff --git a/src/Umbraco.Core/Strings/IShortStringHelper.cs b/src/Umbraco.Abstractions/Strings/IShortStringHelper.cs similarity index 100% rename from src/Umbraco.Core/Strings/IShortStringHelper.cs rename to src/Umbraco.Abstractions/Strings/IShortStringHelper.cs diff --git a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs b/src/Umbraco.Abstractions/Strings/Utf8ToAsciiConverter.cs similarity index 99% rename from src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs rename to src/Umbraco.Abstractions/Strings/Utf8ToAsciiConverter.cs index 54ca74d20f..fea5e2b386 100644 --- a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs +++ b/src/Umbraco.Abstractions/Strings/Utf8ToAsciiConverter.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Strings /// Removes all non-Utf8 (unicode) characters, so in fact it can sort-of "convert" Unicode to Ascii. /// Replaces symbols with '?'. /// - internal static class Utf8ToAsciiConverter + public static class Utf8ToAsciiConverter { /// /// Converts an Utf8 string into an Ascii string. diff --git a/src/Umbraco.Core/Configuration/CommaDelimitedConfigurationElement.cs b/src/Umbraco.Core/Configuration/CommaDelimitedConfigurationElement.cs index 716aa022a5..3ced2fab46 100644 --- a/src/Umbraco.Core/Configuration/CommaDelimitedConfigurationElement.cs +++ b/src/Umbraco.Core/Configuration/CommaDelimitedConfigurationElement.cs @@ -63,7 +63,7 @@ namespace Umbraco.Core.Configuration public void Dispose() { - ObjectExtensions.DisposeIfDisposable(_stringEnumerator); + _stringEnumerator.DisposeIfDisposable(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs index b4c0062770..0833b1811d 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs @@ -3,7 +3,7 @@ using System.Linq; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0 { diff --git a/src/Umbraco.Core/Strings/ContentBaseExtensions.cs b/src/Umbraco.Core/Models/ContentBaseExtensions.cs similarity index 100% rename from src/Umbraco.Core/Strings/ContentBaseExtensions.cs rename to src/Umbraco.Core/Models/ContentBaseExtensions.cs diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 68bc9c923d..59b5ffc407 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -1,504 +1,15 @@ -using System; -using System.Collections; +using Newtonsoft.Json; +using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using System.Linq.Expressions; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Xml; -using Newtonsoft.Json; -using Umbraco.Core.Collections; namespace Umbraco.Core { - /// - /// Provides object extension methods. - /// public static class ObjectExtensions { private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary(); - - private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; - private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter(); - - //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); - - /// - /// - /// - /// - /// - /// - public static IEnumerable AsEnumerableOfOne(this T input) - { - return Enumerable.Repeat(input, 1); - } - - /// - /// - /// - /// - public static void DisposeIfDisposable(this object input) - { - if (input is IDisposable disposable) - disposable.Dispose(); - } - - /// - /// Provides a shortcut way of safely casting an input when you cannot guarantee the is - /// an instance type (i.e., when the C# AS keyword is not applicable). - /// - /// - /// The input. - /// - internal static T SafeCast(this object input) - { - if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default; - if (input is T variable) return variable; - return default; - } - - /// - /// Attempts to convert the input object to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The type to convert to - /// The input. - /// The - public static Attempt TryConvertTo(this object input) - { - var result = TryConvertTo(input, typeof(T)); - - if (result.Success) - return Attempt.Succeed((T)result.Result); - - // just try to cast - try - { - return Attempt.Succeed((T)input); - } - catch (Exception e) - { - return Attempt.Fail(e); - } - } - - /// - /// Attempts to convert the input object to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The input. - /// The type to convert to - /// The - public static Attempt TryConvertTo(this object input, Type target) - { - if (target == null) - { - return Attempt.Fail(); - } - - try - { - if (input == null) - { - // Nullable is ok - if (target.IsGenericType && GetCachedGenericNullableType(target) != null) - { - return Attempt.Succeed(null); - } - - // Reference types are ok - return Attempt.If(target.IsValueType == false, null); - } - - var inputType = input.GetType(); - - // Easy - if (target == typeof(object) || inputType == target) - { - return Attempt.Succeed(input); - } - - // Check for string so that overloaders of ToString() can take advantage of the conversion. - if (target == typeof(string)) - { - return Attempt.Succeed(input.ToString()); - } - - // If we've got a nullable of something, we try to convert directly to that thing. - // We cache the destination type and underlying nullable types - // Any other generic types need to fall through - if (target.IsGenericType) - { - var underlying = GetCachedGenericNullableType(target); - if (underlying != null) - { - // Special case for empty strings for bools/dates which should return null if an empty string. - if (input is string inputString) - { - // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? - if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool))) - { - return Attempt.Succeed(null); - } - } - - // Recursively call into this method with the inner (not-nullable) type and handle the outcome - var inner = input.TryConvertTo(underlying); - - // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception - if (inner.Success) - { - input = inner.Result; // Now fall on through... - } - else - { - return Attempt.Fail(inner.Exception); - } - } - } - else - { - // target is not a generic type - - if (input is string inputString) - { - // Try convert from string, returns an Attempt if the string could be - // processed (either succeeded or failed), else null if we need to try - // other methods - var result = TryConvertToFromString(inputString, target); - if (result.HasValue) - { - return result.Value; - } - } - - // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with - // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. - if (GetCachedCanAssign(input, inputType, target)) - { - return Attempt.Succeed(Convert.ChangeType(input, target)); - } - } - - if (target == typeof(bool)) - { - if (GetCachedCanConvertToBoolean(inputType)) - { - return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input)); - } - } - - var inputConverter = GetCachedSourceTypeConverter(inputType, target); - if (inputConverter != null) - { - return Attempt.Succeed(inputConverter.ConvertTo(input, target)); - } - - var outputConverter = GetCachedTargetTypeConverter(inputType, target); - if (outputConverter != null) - { - return Attempt.Succeed(outputConverter.ConvertFrom(input)); - } - - if (target.IsGenericType && GetCachedGenericNullableType(target) != null) - { - // cannot Convert.ChangeType as that does not work with nullable - // input has already been converted to the underlying type - just - // return input, there's an implicit conversion from T to T? anyways - return Attempt.Succeed(input); - } - - // Re-check convertibles since we altered the input through recursion - if (input is IConvertible convertible2) - { - return Attempt.Succeed(Convert.ChangeType(convertible2, target)); - } - } - catch (Exception e) - { - return Attempt.Fail(e); - } - - return Attempt.Fail(); - } - - /// - /// Attempts to convert the input string to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The input. - /// The type to convert to - /// The - private static Attempt? TryConvertToFromString(this string input, Type target) - { - // Easy - if (target == typeof(string)) - { - return Attempt.Succeed(input); - } - - // Null, empty, whitespaces - if (string.IsNullOrWhiteSpace(input)) - { - if (target == typeof(bool)) - { - // null/empty = bool false - return Attempt.Succeed(false); - } - - if (target == typeof(DateTime)) - { - // null/empty = min DateTime value - return Attempt.Succeed(DateTime.MinValue); - } - - // Cannot decide here, - // Any of the types below will fail parsing and will return a failed attempt - // but anything else will not be processed and will return null - // so even though the string is null/empty we have to proceed. - } - - // Look for type conversions in the expected order of frequency of use. - // - // By using a mixture of ordered if statements and switches we can optimize both for - // fast conditional checking for most frequently used types and the branching - // that does not depend on previous values available to switch statements. - if (target.IsPrimitive) - { - if (target == typeof(int)) - { - if (int.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Because decimal 100.01m will happily convert to integer 100, it - // makes sense that string "100.01" *also* converts to integer 100. - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); - } - - if (target == typeof(long)) - { - if (long.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Same as int - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); - } - - // TODO: Should we do the decimal trick for short, byte, unsigned? - - if (target == typeof(bool)) - { - if (bool.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Don't declare failure so the CustomBooleanTypeConverter can try - return null; - } - - // Calling this method directly is faster than any attempt to cache it. - switch (Type.GetTypeCode(target)) - { - case TypeCode.Int16: - return Attempt.If(short.TryParse(input, out var value), value); - - case TypeCode.Double: - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(double.TryParse(input2, out var valueD), valueD); - - case TypeCode.Single: - var input3 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(float.TryParse(input3, out var valueF), valueF); - - case TypeCode.Char: - return Attempt.If(char.TryParse(input, out var valueC), valueC); - - case TypeCode.Byte: - return Attempt.If(byte.TryParse(input, out var valueB), valueB); - - case TypeCode.SByte: - return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); - - case TypeCode.UInt32: - return Attempt.If(uint.TryParse(input, out var valueU), valueU); - - case TypeCode.UInt16: - return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); - - case TypeCode.UInt64: - return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); - } - } - else if (target == typeof(Guid)) - { - return Attempt.If(Guid.TryParse(input, out var value), value); - } - else if (target == typeof(DateTime)) - { - if (DateTime.TryParse(input, out var value)) - { - switch (value.Kind) - { - case DateTimeKind.Unspecified: - case DateTimeKind.Utc: - return Attempt.Succeed(value); - - case DateTimeKind.Local: - return Attempt.Succeed(value.ToUniversalTime()); - - default: - throw new ArgumentOutOfRangeException(); - } - } - - return Attempt.Fail(); - } - else if (target == typeof(DateTimeOffset)) - { - return Attempt.If(DateTimeOffset.TryParse(input, out var value), value); - } - else if (target == typeof(TimeSpan)) - { - return Attempt.If(TimeSpan.TryParse(input, out var value), value); - } - else if (target == typeof(decimal)) - { - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value), value); - } - else if (input != null && target == typeof(Version)) - { - return Attempt.If(Version.TryParse(input, out var value), value); - } - - // E_NOTIMPL IPAddress, BigInteger - return null; // we can't decide... - } - internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) - { - // TODO: Localize this exception - if (isDisposed) - throw new ObjectDisposedException(objectname); - } - - //public enum PropertyNamesCaseType - //{ - // CamelCase, - // CaseInsensitive - //} - - ///// - ///// Convert an object to a JSON string with camelCase formatting - ///// - ///// - ///// - //public static string ToJsonString(this object obj) - //{ - // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); - //} - - ///// - ///// Convert an object to a JSON string with the specified formatting - ///// - ///// The obj. - ///// Type of the property names case. - ///// - //public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) - //{ - // var type = obj.GetType(); - // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; - - // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) - // { - // return obj.ToString(); - // } - - // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) - // { - // return Convert.ToDateTime(obj).ToString(dateTimeStyle); - // } - - // var serializer = new JsonSerializer(); - - // switch (propertyNamesCaseType) - // { - // case PropertyNamesCaseType.CamelCase: - // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); - // break; - // } - - // var dateTimeConverter = new IsoDateTimeConverter - // { - // DateTimeStyles = System.Globalization.DateTimeStyles.None, - // DateTimeFormat = dateTimeStyle - // }; - - // if (typeof(IDictionary).IsAssignableFrom(type)) - // { - // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - // } - - // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) - // { - // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - // } - - // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - //} - - /// - /// Converts an object into a dictionary - /// - /// - /// - /// - /// - /// - /// - public static IDictionary ToDictionary(this T o, params Expression>[] ignoreProperties) - { - return o.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); - } - - /// - /// Turns object into dictionary - /// - /// - /// Properties to ignore - /// - public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) - { - if (o != null) - { - var props = TypeDescriptor.GetProperties(o); - var d = new Dictionary(); - foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) - { - var val = prop.GetValue(o); - if (val != null) - { - d.Add(prop.Name, (TVal)val); - } - } - return d; - } - return new Dictionary(); - } /// /// Converts an object's properties into a dictionary. @@ -506,10 +17,12 @@ namespace Umbraco.Core /// The object to convert. /// A property namer function. /// A dictionary containing each properties. - public static Dictionary ToObjectDictionary(T obj, Func namer = null) + public static Dictionary ToObjectDictionary(this T obj, Func namer = null) { if (obj == null) return new Dictionary(); + //fixme: This has a hard reference to Newtonsoft + string DefaultNamer(PropertyInfo property) { var jsonProperty = property.GetCustomAttribute(); @@ -530,264 +43,7 @@ namespace Umbraco.Core ToObjectTypes[t] = properties; } - return properties.ToDictionary(x => x.Key, x => ((Func) x.Value)(obj)); + return properties.ToDictionary(x => x.Key, x => ((Func)x.Value)(obj)); } - - internal static string ToDebugString(this object obj, int levels = 0) - { - if (obj == null) return "{null}"; - try - { - if (obj is string) - { - return "\"{0}\"".InvariantFormat(obj); - } - if (obj is int || obj is Int16 || obj is Int64 || obj is float || obj is double || obj is bool || obj is int? || obj is Int16? || obj is Int64? || obj is float? || obj is double? || obj is bool?) - { - return "{0}".InvariantFormat(obj); - } - if (obj is Enum) - { - return "[{0}]".InvariantFormat(obj); - } - if (obj is IEnumerable) - { - var enumerable = (obj as IEnumerable); - - var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); - - return items.Any() - ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) - : null; - } - - var props = obj.GetType().GetProperties(); - if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) - { - try - { - var key = props[0].GetValue(obj, null) as string; - var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); - return "{0}={1}".InvariantFormat(key, value); - } - catch (Exception) - { - return "[KeyValuePropertyException]"; - } - } - if (levels > -1) - { - var items = - (from propertyInfo in props - let value = GetPropertyDebugString(propertyInfo, obj, levels) - where value != null - select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); - - return items.Any() - ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) - : null; - } - } - catch (Exception ex) - { - return "[Exception:{0}]".InvariantFormat(ex.Message); - } - return null; - } - - /// - /// Attempts to serialize the value to an XmlString using ToXmlString - /// - /// - /// - /// - internal static Attempt TryConvertToXmlString(this object value, Type type) - { - try - { - var output = value.ToXmlString(type); - return Attempt.Succeed(output); - } - catch (NotSupportedException ex) - { - return Attempt.Fail(ex); - } - } - - /// - /// Returns an XmlSerialized safe string representation for the value - /// - /// - /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown - /// - internal static string ToXmlString(this object value, Type type) - { - if (value == null) return string.Empty; - if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString()); - if (type == typeof(bool)) return XmlConvert.ToString((bool)value); - if (type == typeof(byte)) return XmlConvert.ToString((byte)value); - if (type == typeof(char)) return XmlConvert.ToString((char)value); - if (type == typeof(DateTime)) return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); - if (type == typeof(DateTimeOffset)) return XmlConvert.ToString((DateTimeOffset)value); - if (type == typeof(decimal)) return XmlConvert.ToString((decimal)value); - if (type == typeof(double)) return XmlConvert.ToString((double)value); - if (type == typeof(float)) return XmlConvert.ToString((float)value); - if (type == typeof(Guid)) return XmlConvert.ToString((Guid)value); - if (type == typeof(int)) return XmlConvert.ToString((int)value); - if (type == typeof(long)) return XmlConvert.ToString((long)value); - if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value); - if (type == typeof(short)) return XmlConvert.ToString((short)value); - if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value); - if (type == typeof(bool)) return XmlConvert.ToString((bool)value); - if (type == typeof(uint)) return XmlConvert.ToString((uint)value); - if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value); - if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value); - - throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert"); - } - - /// - /// Returns an XmlSerialized safe string representation for the value and type - /// - /// - /// - /// - internal static string ToXmlString(this object value) - { - return value.ToXmlString(typeof (T)); - } - - private static string GetEnumPropertyDebugString(object enumItem, int levels) - { - try - { - return enumItem.ToDebugString(levels - 1); - } - catch (Exception) - { - return "[GetEnumPartException]"; - } - } - - private static string GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) - { - try - { - return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); - } - catch (Exception) - { - return "[GetPropertyValueException]"; - } - } - - internal static Guid AsGuid(this object value) - { - return value is Guid guid ? guid : Guid.Empty; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string NormalizeNumberDecimalSeparator(string s) - { - var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; - return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); - } - - // gets a converter for source, that can convert to target, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeConverter GetCachedSourceTypeConverter(Type source, Type target) - { - var key = new CompositeTypeTypeKey(source, target); - - if (InputTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } - - var converter = TypeDescriptor.GetConverter(source); - if (converter.CanConvertTo(target)) - { - return InputTypeConverterCache[key] = converter; - } - - return InputTypeConverterCache[key] = null; - } - - // gets a converter for target, that can convert from source, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeConverter GetCachedTargetTypeConverter(Type source, Type target) - { - var key = new CompositeTypeTypeKey(source, target); - - if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } - - var converter = TypeDescriptor.GetConverter(target); - if (converter.CanConvertFrom(source)) - { - return DestinationTypeConverterCache[key] = converter; - } - - return DestinationTypeConverterCache[key] = null; - } - - // gets the underlying type of a nullable type, or null if the type is not nullable - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Type GetCachedGenericNullableType(Type type) - { - if (NullableGenericCache.TryGetValue(type, out var underlyingType)) - { - return underlyingType; - } - - if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - Type underlying = Nullable.GetUnderlyingType(type); - return NullableGenericCache[type] = underlying; - } - - return NullableGenericCache[type] = null; - } - - // gets an IConvertible from source to target type, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanAssign(object input, Type source, Type target) - { - var key = new CompositeTypeTypeKey(source, target); - if (AssignableTypeCache.TryGetValue(key, out var canConvert)) - { - return canConvert; - } - - // "object is" is faster than "Type.IsAssignableFrom. - // We can use it to very quickly determine whether true/false - if (input is IConvertible && target.IsAssignableFrom(source)) - { - return AssignableTypeCache[key] = true; - } - - return AssignableTypeCache[key] = false; - } - - // determines whether a type can be converted to boolean - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanConvertToBoolean(Type type) - { - if (BoolConvertCache.TryGetValue(type, out var result)) - { - return result; - } - - if (CustomBooleanTypeConverter.CanConvertFrom(type)) - { - return BoolConvertCache[type] = true; - } - - return BoolConvertCache[type] = false; - } - - } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index a25890ff9d..80042dec23 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -11,54 +11,10 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence { - public static class NPocoSqlExtensions + public static partial class NPocoSqlExtensions { - // note: here we take benefit from the fact that NPoco methods that return a Sql, such as - // when doing "sql = sql.Where(...)" actually append to, and return, the original Sql, not - // a new one. - #region Special extensions - - /// - /// Provides a mean to express aliases in SELECT Sql statements. - /// - /// - /// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, - /// then use eg Sql{Foo}(x => Alias(x.Id, "id")). - /// - public static class Statics - { - /// - /// Aliases a field. - /// - /// The field to alias. - /// The alias. - public static object Alias(object field, string alias) => field; - - /// - /// Produces Sql text. - /// - /// The name of the field. - /// A function producing Sql text. - public static T SqlText(string field, Func expr) => default; - - /// - /// Produces Sql text. - /// - /// The name of the first field. - /// The name of the second field. - /// A function producing Sql text. - public static T SqlText(string field1, string field2, Func expr) => default; - - /// - /// Produces Sql text. - /// - /// The name of the first field. - /// The name of the second field. - /// The name of the third field. - /// A function producing Sql text. - public static T SqlText(string field1, string field2, string field3, Func expr) => default; - } +#region Special extensions #endregion diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index d04930fa92..ab4f59591f 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -619,7 +619,7 @@ namespace Umbraco.Core.Persistence.Querying // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); case "SqlText": - if (m.Method.DeclaringType != typeof(NPocoSqlExtensions.Statics)) + if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) goto default; if (m.Arguments.Count == 2) { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index aeb4c3774f..d33fbc470b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -10,14 +10,13 @@ using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs index 02ba4d1c13..6dca479a92 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -15,7 +15,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index b4c0e51c22..b853c2e026 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -4,11 +4,10 @@ using System.Linq; using NPoco; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Services; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 65043c4c67..c679a3cbd8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.RegularExpressions; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -13,7 +12,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 808f61305a..fdf75d8a4c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs index 4c02a8f4b5..6823a00f94 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs @@ -63,7 +63,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries ids = ids - .Select(x => StringExtensions.EnsureEndsWith(x, ".css")) + .Select(x => x.EnsureEndsWith(".css")) .Distinct() .ToArray(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs index f26fcca81b..fb836dccac 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 4df1105bf7..77ffb10192 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1,16 +1,11 @@ -using System; +using Newtonsoft.Json; +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.Threading.Tasks; using System.Web.Security; -using Newtonsoft.Json; -using Umbraco.Core.Configuration; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -18,69 +13,8 @@ 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 @@ -125,27 +59,6 @@ namespace Umbraco.Core 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 /// @@ -168,93 +81,6 @@ namespace Umbraco.Core } } - 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 /// @@ -314,749 +140,6 @@ namespace Umbraco.Core 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 /// @@ -1221,270 +304,5 @@ namespace Umbraco.Core { 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; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6a98418171..90232a3cbc 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -157,7 +157,6 @@ - @@ -223,8 +222,10 @@ + + @@ -235,6 +236,12 @@ + + + + + + @@ -551,7 +558,6 @@ - @@ -635,7 +641,6 @@ - @@ -661,7 +666,6 @@ - @@ -904,7 +908,6 @@ - @@ -1461,19 +1464,6 @@ - - - - - - - - - - - - - @@ -1505,7 +1495,6 @@ - @@ -1515,6 +1504,7 @@ Properties\SolutionInfo.cs + diff --git a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs index 0431e7d69f..2297be0de8 100644 --- a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs +++ b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs @@ -518,7 +518,7 @@ namespace Umbraco.Tests.Clr // test object extensions Console.WriteLine("Getting object5D values..."); - var values5D = ObjectExtensions.ToObjectDictionary(object5); + var values5D = object5.ToObjectDictionary(); Console.WriteLine("Writing object5D values..."); foreach ((var name, var value) in values5) diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs index 59a046558c..373de9e654 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Tests.TestHelpers; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Tests.Persistence.NPocoTests { diff --git a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs index 8f56c8f246..9819e7cd64 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs @@ -246,7 +246,7 @@ namespace Umbraco.Tests.Persistence.Querying //Console.WriteLine(result2); - Expression> predicate3 = (user, group) => NPocoSqlExtensions.Statics.SqlText(user.Login, group.Name, (n1, n2) => $"({n1} LIKE concat({n2}, ',%'))"); + Expression> predicate3 = (user, group) => SqlExtensionsStatics.SqlText(user.Login, group.Name, (n1, n2) => $"({n1} LIKE concat({n2}, ',%'))"); var modelToSqlExpressionHelper3 = new PocoToSqlExpressionVisitor(SqlContext, null, null); var result3 = modelToSqlExpressionHelper3.Visit(predicate3); diff --git a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs index 4c365d733f..e0ad65cf8b 100644 --- a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs +++ b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs @@ -210,10 +210,10 @@ namespace Umbraco.Tests.Strings { var bytes = Encoding.UTF8.GetBytes(value); Console.WriteLine("base64: " + Convert.ToBase64String(bytes)); - var encoded = StringExtensions.UrlTokenEncode(bytes); + var encoded = bytes.UrlTokenEncode(); Assert.AreEqual(expected, encoded); - var backBytes = StringExtensions.UrlTokenDecode(encoded); + var backBytes = encoded.UrlTokenDecode(); var backString = Encoding.UTF8.GetString(backBytes); Assert.AreEqual(value, backString); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index dcb3f44487..60dcbb989b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; using Umbraco.Core.Serialization; using Umbraco.Web.Composing; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Web.PublishedCache.NuCache.DataSource {