From 16ed68d5de5add1ee017bc3181f08e7c835d4b9f Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 10 Jul 2022 12:25:42 -0500 Subject: [PATCH] New: Custom Format Spec Validation Fixes #7405 --- .../CustomFormatSpecificationBase.cs | 3 +++ .../ICustomFormatSpecification.cs | 3 +++ .../IndexerFlagSpecification.cs | 25 +++++++++++++++++ .../Specifications/LanguageSpecification.cs | 25 +++++++++++++++++ .../QualityModifierSpecification.cs | 25 +++++++++++++++++ .../Specifications/RegexSpecificationBase.cs | 24 ++++++++++++++++- .../Specifications/ResolutionSpecification.cs | 17 ++++++++++++ .../Specifications/SizeSpecification.cs | 18 +++++++++++++ .../Specifications/SourceSpecification.cs | 17 ++++++++++++ .../CustomFormats/CustomFormatController.cs | 27 +++++++++++++++++++ 10 files changed, 183 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs b/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs index 7b5cc29d1..8b3610269 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { @@ -18,6 +19,8 @@ namespace NzbDrone.Core.CustomFormats return (ICustomFormatSpecification)MemberwiseClone(); } + public abstract NzbDroneValidationResult Validate(); + public bool IsSatisfiedBy(ParsedMovieInfo movieInfo) { var match = IsSatisfiedByWithoutNegate(movieInfo); diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs index 001bc1f7b..d15188f9f 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { @@ -11,6 +12,8 @@ namespace NzbDrone.Core.CustomFormats bool Negate { get; set; } bool Required { get; set; } + NzbDroneValidationResult Validate(); + ICustomFormatSpecification Clone(); bool IsSatisfiedBy(ParsedMovieInfo movieInfo); } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs index 7aac93c54..47efd2cf4 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -1,11 +1,31 @@ +using System; using System.Collections.Generic; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class IndexerFlagSpecificationValidator : AbstractValidator + { + public IndexerFlagSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + RuleFor(c => c.Value).Custom((qualityValue, context) => + { + if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue)) + { + context.AddFailure(string.Format("Invalid indexer flag condition value: {0}", qualityValue)); + } + }); + } + } + public class IndexerFlagSpecification : CustomFormatSpecificationBase { + private static readonly IndexerFlagSpecificationValidator Validator = new IndexerFlagSpecificationValidator(); + public override int Order => 4; public override string ImplementationName => "Indexer Flag"; @@ -17,5 +37,10 @@ namespace NzbDrone.Core.CustomFormats var flags = movieInfo?.ExtraInfo?.GetValueOrDefault("IndexerFlags") as IndexerFlags?; return flags?.HasFlag((IndexerFlags)Value) == true; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs index db7f40389..c211ace31 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs @@ -1,11 +1,31 @@ +using System.Linq; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class LanguageSpecificationValidator : AbstractValidator + { + public LanguageSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + RuleFor(c => c.Value).Custom((value, context) => + { + if (!Language.All.Any(o => o.Id == value)) + { + context.AddFailure(string.Format("Invalid Language condition value: {0}", value)); + } + }); + } + } + public class LanguageSpecification : CustomFormatSpecificationBase { + private static readonly LanguageSpecificationValidator Validator = new LanguageSpecificationValidator(); + public override int Order => 3; public override string ImplementationName => "Language"; @@ -19,5 +39,10 @@ namespace NzbDrone.Core.CustomFormats : (Language)Value; return movieInfo?.Languages?.Contains(comparedLanguage) ?? false; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/QualityModifierSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/QualityModifierSpecification.cs index 1bad22392..21db0a515 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/QualityModifierSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/QualityModifierSpecification.cs @@ -1,11 +1,31 @@ +using System; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class QualityModifierSpecificationValidator : AbstractValidator + { + public QualityModifierSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + RuleFor(c => c.Value).Custom((qualityValue, context) => + { + if (!Enum.IsDefined(typeof(Modifier), qualityValue)) + { + context.AddFailure(string.Format("Invalid quality modifier condition value: {0}", qualityValue)); + } + }); + } + } + public class QualityModifierSpecification : CustomFormatSpecificationBase { + private static readonly QualityModifierSpecificationValidator Validator = new QualityModifierSpecificationValidator(); + public override int Order => 7; public override string ImplementationName => "Quality Modifier"; @@ -16,5 +36,10 @@ namespace NzbDrone.Core.CustomFormats { return (movieInfo?.Quality?.Quality?.Modifier ?? (int)Modifier.NONE) == (Modifier)Value; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs b/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs index 030dff60a..ead5481eb 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs @@ -1,10 +1,23 @@ using System.Text.RegularExpressions; +using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class RegexSpecificationBaseValidator : AbstractValidator + { + public RegexSpecificationBaseValidator() + { + RuleFor(c => c.Value).NotEmpty().WithMessage("Regex Pattern must not be empty"); + } + } + public abstract class RegexSpecificationBase : CustomFormatSpecificationBase { + private static readonly RegexSpecificationBaseValidator Validator = new RegexSpecificationBaseValidator(); + protected Regex _regex; protected string _raw; @@ -15,7 +28,11 @@ namespace NzbDrone.Core.CustomFormats set { _raw = value; - _regex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase); + + if (value.IsNotNullOrWhiteSpace()) + { + _regex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } } } @@ -28,5 +45,10 @@ namespace NzbDrone.Core.CustomFormats return _regex.IsMatch(compared); } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs index 279289651..5bcd1b72f 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs @@ -1,11 +1,23 @@ +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class ResolutionSpecificationValidator : AbstractValidator + { + public ResolutionSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + } + } + public class ResolutionSpecification : CustomFormatSpecificationBase { + private static readonly ResolutionSpecificationValidator Validator = new ResolutionSpecificationValidator(); + public override int Order => 6; public override string ImplementationName => "Resolution"; @@ -16,5 +28,10 @@ namespace NzbDrone.Core.CustomFormats { return (movieInfo?.Quality?.Quality?.Resolution ?? (int)Resolution.Unknown) == Value; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs index 1124783fb..ae54458b5 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs @@ -1,11 +1,24 @@ using System.Collections.Generic; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class SizeSpecificationValidator : AbstractValidator + { + public SizeSpecificationValidator() + { + RuleFor(c => c.Min).GreaterThan(0); + RuleFor(c => c.Max).GreaterThan(c => c.Min); + } + } + public class SizeSpecification : CustomFormatSpecificationBase { + private static readonly SizeSpecificationValidator Validator = new SizeSpecificationValidator(); + public override int Order => 8; public override string ImplementationName => "Size"; @@ -21,5 +34,10 @@ namespace NzbDrone.Core.CustomFormats return size > Min.Gigabytes() && size <= Max.Gigabytes(); } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs index a5b8e4c8c..b40f39823 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs @@ -1,11 +1,23 @@ +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { + public class SourceSpecificationValidator : AbstractValidator + { + public SourceSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + } + } + public class SourceSpecification : CustomFormatSpecificationBase { + private static readonly SourceSpecificationValidator Validator = new SourceSpecificationValidator(); + public override int Order => 5; public override string ImplementationName => "Source"; @@ -16,5 +28,10 @@ namespace NzbDrone.Core.CustomFormats { return (movieInfo?.Quality?.Quality?.Source ?? (int)Source.UNKNOWN) == (Source)Value; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/Radarr.Api.V3/CustomFormats/CustomFormatController.cs b/src/Radarr.Api.V3/CustomFormats/CustomFormatController.cs index 30793de28..5ca6a15c7 100644 --- a/src/Radarr.Api.V3/CustomFormats/CustomFormatController.cs +++ b/src/Radarr.Api.V3/CustomFormats/CustomFormatController.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; using System.Linq; using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Validation; using Radarr.Http; using Radarr.Http.REST; using Radarr.Http.REST.Attributes; @@ -50,6 +52,9 @@ namespace Radarr.Api.V3.CustomFormats public ActionResult Create(CustomFormatResource customFormatResource) { var model = customFormatResource.ToModel(_specifications); + + Validate(model); + return Created(_formatService.Insert(model).Id); } @@ -57,6 +62,9 @@ namespace Radarr.Api.V3.CustomFormats public ActionResult Update(CustomFormatResource resource) { var model = resource.ToModel(_specifications); + + Validate(model); + _formatService.Update(model); return Accepted(model.Id); @@ -89,6 +97,25 @@ namespace Radarr.Api.V3.CustomFormats return schema; } + private void Validate(CustomFormat definition) + { + foreach (var spec in definition.Specifications) + { + var validationResult = spec.Validate(); + VerifyValidationResult(validationResult); + } + } + + protected void VerifyValidationResult(ValidationResult validationResult) + { + var result = new NzbDroneValidationResult(validationResult.Errors); + + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + } + private IEnumerable GetPresets() { yield return new ReleaseTitleSpecification