diff --git a/EXILED/Exiled.API/Features/Attributes/ValidateChildrenAttribute.cs b/EXILED/Exiled.API/Features/Attributes/ValidateChildrenAttribute.cs new file mode 100644 index 0000000000..b31fe9f6e4 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/ValidateChildrenAttribute.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes +{ + using System; + + /// + /// Checks all properties in the target object for validators. + /// + [AttributeUsage(AttributeTargets.Property)] + public class ValidateChildrenAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs new file mode 100644 index 0000000000..a92da9244a --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Checks if value is in list of available values. + /// + [AttributeUsage(AttributeTargets.Property)] + public class AvailableValuesAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + public AvailableValuesAttribute(params object[] values) + { + Values = values; + } + + /// + /// Gets the array of possible values. + /// + public object[] Values { get; } + + /// + public bool Check(object other) => Values.Contains(other); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs new file mode 100644 index 0000000000..611b416070 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + using System.Collections.Generic; + + using Exiled.API.Interfaces; + + /// + /// Check a value with custom function. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class CustomValidatorAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// The type of the custom check validator. + /// + /// The from customFunctionType must be a class inheriting IValidator with a parameterless constructor. + /// + public CustomValidatorAttribute(Type customFunctionType) + { + if (!customFunctionType.IsClass || customFunctionType.IsAbstract || !customFunctionType.GetInterfaces().Contains(typeof(IValidator))) + throw new ArgumentException($"{nameof(customFunctionType)} must be a type inheriting IValidator!"); + + CustomFunctionType = customFunctionType; + } + + /// + /// Gets a from a type inheriting , to an instance of that class. + /// + public static Dictionary ValidatorInstances { get; } = new(); + + /// + /// Gets the type of the custom check validator. + /// + public Type CustomFunctionType { get; } + + /// + public bool Check(object other) => ValidatorInstances.GetOrAdd(CustomFunctionType, () => (IValidator)Activator.CreateInstance(CustomFunctionType)).Check(other); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs new file mode 100644 index 0000000000..b31d3cbc83 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Checks if value greater or equal. + /// + [AttributeUsage(AttributeTargets.Property)] + public class GreaterOrEqualAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + /// value must be able to convert to your target type via . + public GreaterOrEqualAttribute(object value) + { + Value = value; + } + + /// + /// Gets the minimum value. + /// + public object Value { get; } + + /// + public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable min && min.CompareTo(other) <= 0; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs new file mode 100644 index 0000000000..5bb1025bd9 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Interfaces; + + /// + /// Check if value is greater. + /// + [AttributeUsage(AttributeTargets.Property)] + public class GreaterThanAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + /// value must be able to convert to your target type via . + public GreaterThanAttribute(object value) + { + Value = value; + } + + /// + /// Gets the minimum value. + /// + public object Value { get; } + + /// + public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable min && min.CompareTo(other) < 0; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs new file mode 100644 index 0000000000..e521a47bd1 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Checks if value less or equal. + /// + [AttributeUsage(AttributeTargets.Property)] + public class LessOrEqualAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + /// value must be able to convert to your target type via . + public LessOrEqualAttribute(object value) + { + Value = value; + } + + /// + /// Gets the maximum value. + /// + public object Value { get; } + + /// + public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable max && max.CompareTo(other) >= 0; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs new file mode 100644 index 0000000000..5497a99fa6 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Checks if value is less. + /// + [AttributeUsage(AttributeTargets.Property)] + public class LessThanAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + /// value must be able to convert to your target type via . + public LessThanAttribute(object value) + { + Value = value; + } + + /// + /// Gets the maximum value. + /// + public object Value { get; } + + /// + public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable max && max.CompareTo(other) > 0; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs new file mode 100644 index 0000000000..914ce09d59 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs @@ -0,0 +1,23 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Checks if value is 0 or greater. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NonNegativeAttribute : Attribute, IValidator + { + /// + public bool Check(object other) => Convert.ToDecimal(other) >= 0; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs new file mode 100644 index 0000000000..a473405e49 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs @@ -0,0 +1,23 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Check if value is 0 or less. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NonPositiveAttribute : Attribute, IValidator + { + /// + public bool Check(object other) => Convert.ToDecimal(other) <= 0; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs new file mode 100644 index 0000000000..ebf0c1d10d --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs @@ -0,0 +1,57 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + using Exiled.API.Interfaces; + + /// + /// Check if an is inside a specific range. + /// + [AttributeUsage(AttributeTargets.Property)] + public class RangeAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// min and max must inherit . + public RangeAttribute(object min, object max, bool inclusive = false) + { + Min = min; + Max = max; + Inclusive = inclusive; + } + + /// + /// Gets the maximum value. + /// + public object Max { get; } + + /// + /// Gets the minimum value. + /// + public object Min { get; } + + /// + /// Gets a value indicating whether check is inclusive. + /// + public bool Inclusive { get; } + + /// + public bool Check(object other) => + Convert.ChangeType(Min, other.GetType()) is IComparable min && + Convert.ChangeType(Max, other.GetType()) is IComparable max && + (Inclusive + ? min.CompareTo(other) <= 0 && max.CompareTo(other) >= 0 + : min.CompareTo(other) < 0 && max.CompareTo(other) > 0); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Interfaces/IValidator.cs b/EXILED/Exiled.API/Interfaces/IValidator.cs new file mode 100644 index 0000000000..f9e2004ee1 --- /dev/null +++ b/EXILED/Exiled.API/Interfaces/IValidator.cs @@ -0,0 +1,22 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Interfaces +{ + /// + /// Interface for all validations attributes. + /// + public interface IValidator + { + /// + /// Checks if is satisfying this attributes condition. + /// + /// Value to check. + /// Whether the value has passed check. + public bool Check(object other); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Loader/Config.cs b/EXILED/Exiled.Loader/Config.cs index 6e07979ef2..4e720d7bb7 100644 --- a/EXILED/Exiled.Loader/Config.cs +++ b/EXILED/Exiled.Loader/Config.cs @@ -88,5 +88,11 @@ public sealed class Config : IConfig /// [Description("Indicates whether Exiled should auto-update itself as soon as a new release is available.")] public bool EnableAutoUpdates { get; set; } = true; + + /// + /// Gets or sets a value indicating whether config validator should check all properties inside config values' types. + /// + [Description("Indicating whether config validator should check all properties inside config values' types.")] + public bool EnableDeepValidation { get; set; } = false; } } \ No newline at end of file diff --git a/EXILED/Exiled.Loader/ConfigManager.cs b/EXILED/Exiled.Loader/ConfigManager.cs index 706924ee45..ac52381049 100644 --- a/EXILED/Exiled.Loader/ConfigManager.cs +++ b/EXILED/Exiled.Loader/ConfigManager.cs @@ -18,6 +18,7 @@ namespace Exiled.Loader using API.Interfaces; using Exiled.API.Features; + using Exiled.API.Features.Attributes; using Exiled.API.Features.Pools; using LabApi.Loader.Features.Plugins.Configuration; @@ -72,6 +73,93 @@ public static SortedDictionary LoadSorted(string rawConfigs) } } + /// + /// Validates plugin config. + /// + /// Plugin which config is validated. + /// Validated config. + /// Config after validation is passed. + public static IConfig ValidateConfig(this IPlugin plugin, IConfig config) + { + int validated = 0; + foreach (PropertyInfo propertyInfo in config.GetType().GetProperties().Where(x => x.GetMethod != null && x.SetMethod != null)) + { + try + { + ValidateType(config, plugin.Config, propertyInfo, ref validated); + } + catch (Exception ex) + { + Log.Error($"Failed to validate config: {ex}"); + } + } + + if (validated > 0) + Log.Info($"Plugin {plugin.Name} has successfully passed {validated} config validations!"); + + return config; + } + + /// + /// Performs a validation for property and all its properties in 's type. + /// + /// Plugin which config is validated. + /// Validated config. + /// Property which will be validated. + /// Amount of successfully passed validations. + public static void ValidateType(object instance, object defaultInstance, PropertyInfo propertyInfo, ref int validated) + { + object value = propertyInfo.GetValue(instance, null); + object defaultValue = propertyInfo.GetValue(defaultInstance, null); + + bool hasValidateChildrenAttribute = false; + try + { + foreach (Attribute attribute in propertyInfo.GetCustomAttributes()) + { + hasValidateChildrenAttribute |= attribute is ValidateChildrenAttribute; + if (attribute is not IValidator validator) + continue; + + try + { + if (!validator.Check(value)) + { + Log.Error($"Value {value} in config ({propertyInfo.Name.ToSnakeCase()}) has failed validation for attribute {attribute.GetType().Name}. Default value ({defaultValue}) will be used instead."); + propertyInfo.SetValue(instance, defaultValue); + continue; + } + } + catch (Exception ex) + { + Log.Error($"Value {value} in config ({propertyInfo.Name.ToSnakeCase()}) has failed validation for attribute {attribute.GetType().Name}. Default value ({defaultValue}) will be used instead."); + Log.Error($"Validation error message: {ex.Message}"); + propertyInfo.SetValue(instance, defaultValue); + continue; + } + + validated++; + } + } + catch (Exception ex) + { + Log.Error($"Error while validating value of property '{propertyInfo.Name}': {ex.Message}. Default value ({defaultValue}) will be used instead."); + return; + } + + if (hasValidateChildrenAttribute || (!LoaderPlugin.Config.EnableDeepValidation && !(propertyInfo.PropertyType.Namespace?.Contains("System") ?? false))) + { + foreach (PropertyInfo property in propertyInfo.PropertyType.GetProperties().Where(x => x.GetMethod != null && x.SetMethod != null)) + { + ConstructorInfo ctor = property.PropertyType.GetConstructor(Type.EmptyTypes); + if (ctor is null) + continue; + + ValidateType(value, ctor.Invoke(null, null), property, ref validated); + } + } + } + /// /// Loads the config of a plugin using the distribution. /// @@ -106,7 +194,7 @@ public static IConfig LoadDefaultConfig(this IPlugin plugin, Dictionary try { string rawConfigString = Loader.Serializer.Serialize(rawDeserializedConfig); - config = (IConfig)Loader.Deserializer.Deserialize(rawConfigString, plugin.Config.GetType()); + config = ValidateConfig(plugin, (IConfig)Loader.Deserializer.Deserialize(rawConfigString, plugin.Config.GetType())); plugin.Config.CopyProperties(config); } catch (YamlException yamlException) @@ -135,7 +223,7 @@ public static IConfig LoadSeparatedConfig(this IPlugin plugin) try { - config = (IConfig)Loader.Deserializer.Deserialize(File.ReadAllText(plugin.ConfigPath), plugin.Config.GetType()); + config = ValidateConfig(plugin, (IConfig)Loader.Deserializer.Deserialize(File.ReadAllText(plugin.ConfigPath), plugin.Config.GetType())); plugin.Config.CopyProperties(config); } catch (YamlException yamlException)