You've already forked Sonarr
							
							
				mirror of
				https://github.com/Sonarr/Sonarr.git
				synced 2025-10-31 00:07:55 +02:00 
			
		
		
		
	added path validation to add series/ recent folders.
This commit is contained in:
		| @@ -164,6 +164,7 @@ | ||||
|     <Compile Include="System\SystemModule.cs" /> | ||||
|     <Compile Include="TinyIoCNancyBootstrapper.cs" /> | ||||
|     <Compile Include="Update\UpdateModule.cs" /> | ||||
|     <Compile Include="Validation\PathValidator.cs" /> | ||||
|     <Compile Include="Validation\RuleBuilderExtensions.cs" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| using System.Collections.Generic; | ||||
| using NzbDrone.Core.RootFolders; | ||||
| using NzbDrone.Api.Mapping; | ||||
| using NzbDrone.Api.Validation; | ||||
|  | ||||
| namespace NzbDrone.Api.RootFolders | ||||
| { | ||||
| @@ -17,6 +18,8 @@ namespace NzbDrone.Api.RootFolders | ||||
|             GetResourceById = GetRootFolder; | ||||
|             CreateResource = CreateRootFolder; | ||||
|             DeleteResource = DeleteFolder; | ||||
|  | ||||
|             SharedValidator.RuleFor(c=>c.Path).IsValidPath(); | ||||
|         } | ||||
|  | ||||
|         private RootFolderResource GetRootFolder(int id) | ||||
|   | ||||
| @@ -31,10 +31,10 @@ namespace NzbDrone.Api.Series | ||||
|  | ||||
|             SharedValidator.RuleFor(s => s.QualityProfileId).ValidId(); | ||||
|  | ||||
|             PutValidator.RuleFor(s => s.Path).NotEmpty(); | ||||
|             PutValidator.RuleFor(s => s.Path).IsValidPath(); | ||||
|  | ||||
|             PostValidator.RuleFor(s => s.Path).NotEmpty().When(s => String.IsNullOrEmpty(s.RootFolderPath)); | ||||
|             PostValidator.RuleFor(s => s.RootFolderPath).NotEmpty().When(s => String.IsNullOrEmpty(s.Path)); | ||||
|             PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => String.IsNullOrEmpty(s.RootFolderPath)); | ||||
|             PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path)); | ||||
|             PostValidator.RuleFor(s => s.Title).NotEmpty(); | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										18
									
								
								NzbDrone.Api/Validation/PathValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								NzbDrone.Api/Validation/PathValidator.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| using FluentValidation.Validators; | ||||
| using NzbDrone.Common; | ||||
|  | ||||
| namespace NzbDrone.Api.Validation | ||||
| { | ||||
|     public class PathValidator : PropertyValidator | ||||
|     { | ||||
|         public PathValidator() | ||||
|             : base("Invalid Path") | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         protected override bool IsValid(PropertyValidatorContext context) | ||||
|         { | ||||
|             return context.PropertyValue.ToString().IsPathValid(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,6 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using System; | ||||
| using System.Linq.Expressions; | ||||
| using System.Text.RegularExpressions; | ||||
| using FluentValidation; | ||||
| using FluentValidation.Validators; | ||||
|  | ||||
| @@ -20,5 +22,10 @@ namespace NzbDrone.Api.Validation | ||||
|         { | ||||
|             return ruleBuilder.SetValidator(new RegularExpressionValidator("^http(s)?://", RegexOptions.IgnoreCase)).WithMessage("must start with http:// or https://"); | ||||
|         } | ||||
|  | ||||
|         public static IRuleBuilderOptions<T, string> IsValidPath<T>(this IRuleBuilder<T, string> ruleBuilder) | ||||
|         { | ||||
|             return ruleBuilder.SetValidator(new PathValidator()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -95,8 +95,7 @@ namespace NzbDrone.Common.EnsureThat | ||||
|             return param; | ||||
|         } | ||||
|  | ||||
|         private static readonly Regex windowsInvalidPathRegex = new Regex(@"[/*<>""|]", RegexOptions.Compiled); | ||||
|         private static readonly Regex windowsPathRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled); | ||||
|  | ||||
|  | ||||
|         [DebuggerStepThrough] | ||||
|         public static Param<string> IsValidPath(this Param<string> param) | ||||
| @@ -104,31 +103,14 @@ namespace NzbDrone.Common.EnsureThat | ||||
|             if (string.IsNullOrWhiteSpace(param.Value)) | ||||
|                 throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace); | ||||
|  | ||||
|             if (param.Value.IsPathValid()) return param; | ||||
|  | ||||
|             if (OsInfo.IsLinux) | ||||
|             { | ||||
|                 if (!param.Value.StartsWith(Path.DirectorySeparatorChar.ToString())) | ||||
|                 { | ||||
|                     throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}]  is not a valid *nix path. paths must start with /", param.Value)); | ||||
|                 } | ||||
|                 throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}]  is not a valid *nix path. paths must start with /", param.Value)); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (windowsInvalidPathRegex.IsMatch(param.Value)) | ||||
|                 { | ||||
|                     throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}]  is not a valid Windows path. It contains invalid characters", param.Value)); | ||||
|                 } | ||||
|  | ||||
|                 //Network path | ||||
|                 if (param.Value.StartsWith(Path.DirectorySeparatorChar.ToString())) return param; | ||||
|  | ||||
|                 if (!windowsPathRegex.IsMatch(param.Value)) | ||||
|                 { | ||||
|                     throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}]  is not a valid Windows path. paths must be a full path eg. C:\\Windows", param.Value)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|  | ||||
|             return param; | ||||
|           | ||||
|             throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}]  is not a valid Windows path. paths must be a full path eg. C:\\Windows", param.Value)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System; | ||||
| using System.IO; | ||||
| using System.Text.RegularExpressions; | ||||
| using NzbDrone.Common.EnsureThat; | ||||
| using NzbDrone.Common.EnvironmentInfo; | ||||
|  | ||||
| @@ -45,6 +46,32 @@ namespace NzbDrone.Common | ||||
|             return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), StringComparison.InvariantCultureIgnoreCase); | ||||
|         } | ||||
|  | ||||
|         private static readonly Regex WindowsInvalidPathRegex = new Regex(@"[/*<>""|]", RegexOptions.Compiled); | ||||
|         private static readonly Regex WindowsPathRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled); | ||||
|  | ||||
|         public static bool IsPathValid(this string path) | ||||
|         { | ||||
|             if (OsInfo.IsLinux && !path.StartsWith(Path.DirectorySeparatorChar.ToString())) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             if (WindowsInvalidPathRegex.IsMatch(path)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             //Network path | ||||
|             if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) return true; | ||||
|  | ||||
|             if (!WindowsPathRegex.IsMatch(path)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         public static bool ContainsInvalidPathChars(this string text) | ||||
|         { | ||||
|             return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0; | ||||
|   | ||||
| @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Validation | ||||
|  | ||||
|         public static IRuleBuilderOptions<T, string> ValidRootUrl<T>(this IRuleBuilder<T, string> ruleBuilder) | ||||
|         { | ||||
|             ruleBuilder.SetValidator(new NotEmptyValidator(null)); | ||||
|             return ruleBuilder.SetValidator(new RegularExpressionValidator("^http(?:s)?://[a-z0-9-.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that"); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using NLog; | ||||
| using System.Runtime.CompilerServices; | ||||
| using NLog; | ||||
| using NLog.Config; | ||||
| using NLog.Targets; | ||||
| using NUnit.Framework; | ||||
| @@ -39,7 +40,7 @@ namespace NzbDrone.Integration.Test | ||||
|             LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget)); | ||||
|         } | ||||
|  | ||||
|         [SetUp] | ||||
|         [TestFixtureSetUp] | ||||
|         public void SmokeTestSetup() | ||||
|         { | ||||
|             _runner = new NzbDroneRunner(); | ||||
| @@ -63,7 +64,7 @@ namespace NzbDrone.Integration.Test | ||||
|             NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming"); | ||||
|         } | ||||
|  | ||||
|         [TearDown] | ||||
|         [TestFixtureTearDown] | ||||
|         public void SmokeTestTearDown() | ||||
|         { | ||||
|             _runner.KillAll(); | ||||
|   | ||||
| @@ -58,5 +58,17 @@ namespace NzbDrone.Integration.Test | ||||
|  | ||||
|             RootFolders.All().Should().BeEmpty(); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         public void invalid_path_should_return_bad_request() | ||||
|         { | ||||
|             var rootFolder = new RootFolderResource | ||||
|             { | ||||
|                 Path = "invalid_path" | ||||
|             }; | ||||
|  | ||||
|             var postResponse = RootFolders.InvalidPost(rootFolder); | ||||
|             postResponse.Should().NotBeEmpty(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -9,12 +9,6 @@ namespace NzbDrone.Integration.Test | ||||
|     [TestFixture] | ||||
|     public class SeriesIntegrationTest : IntegrationTest | ||||
|     { | ||||
|         [Test] | ||||
|         public void should_have_no_series_on_start_application() | ||||
|         { | ||||
|             Series.All().Should().BeEmpty(); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         public void series_lookup_on_trakt() | ||||
|         { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| 'use strict'; | ||||
| 'use strict'; | ||||
| define( | ||||
|     [ | ||||
|         'backbone', | ||||
|   | ||||
| @@ -7,10 +7,11 @@ define( | ||||
|         'AddSeries/RootFolders/Collection', | ||||
|         'AddSeries/RootFolders/Model', | ||||
|         'Shared/LoadingView', | ||||
|         'Mixins/AsValidatedView', | ||||
|         'Mixins/AutoComplete' | ||||
|     ], function (Marionette, RootFolderCollectionView, RootFolderCollection, RootFolderModel, LoadingView) { | ||||
|     ], function (Marionette, RootFolderCollectionView, RootFolderCollection, RootFolderModel, LoadingView, AsValidatedView) { | ||||
|  | ||||
|         return Marionette.Layout.extend({ | ||||
|         var layout = Marionette.Layout.extend({ | ||||
|             template: 'AddSeries/RootFolders/LayoutTemplate', | ||||
|  | ||||
|             ui: { | ||||
| @@ -55,12 +56,16 @@ define( | ||||
|                     Path: this.ui.pathInput.val() | ||||
|                 }); | ||||
|  | ||||
|                 RootFolderCollection.add(newDir); | ||||
|                 this.bindToModelValidation(newDir); | ||||
|  | ||||
|                 newDir.save().done(function () { | ||||
|                     RootFolderCollection.add(newDir); | ||||
|                     self.trigger('folderSelected', {model: newDir}); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         return AsValidatedView.apply(layout); | ||||
|  | ||||
|     }); | ||||
|   | ||||
| @@ -3,10 +3,13 @@ | ||||
|     <h3>Select Folder</h3> | ||||
| </div> | ||||
| <div class="modal-body root-folders-modal"> | ||||
|     <div class="input-prepend input-append x-path"> | ||||
|     <div class="validation-errors"></div> | ||||
|     <div class="input-prepend input-append x-path control-group"> | ||||
|         <span class="add-on"> <i class="icon-folder-open"></i></span> | ||||
|         <input class="span9" type="text" placeholder="Start Typing Folder Path..."> | ||||
|         <button class="btn btn-success x-add"><i class="icon-ok"/></button> | ||||
|         <input class="span9" type="text" validation-name="path" placeholder="Start Typing Folder Path..."> | ||||
|         <button class="btn btn-success x-add"> | ||||
|             <i class="icon-ok"/> | ||||
|         </button> | ||||
|     </div> | ||||
|     {{#if items}} | ||||
|     <h4>Recent Folders</h4> | ||||
|   | ||||
| @@ -4,6 +4,7 @@ define( | ||||
|         'backbone' | ||||
|     ], function (Backbone) { | ||||
|         return Backbone.Model.extend({ | ||||
|             urlRoot : window.ApiRoot + '/rootfolder', | ||||
|             defaults: { | ||||
|                 freeSpace: 0 | ||||
|             } | ||||
|   | ||||
| @@ -38,7 +38,7 @@ define( | ||||
|  | ||||
|                 this.listenTo(App.vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); | ||||
|                 this.listenTo(this.model, 'change', this.render); | ||||
|                 this.listenTo(RootFolders, 'change', this.render); | ||||
|                 this.listenTo(RootFolders, 'all', this.render); | ||||
|  | ||||
|                 this.rootFolderLayout = new RootFolderLayout(); | ||||
|                 this.listenTo(this.rootFolderLayout, 'folderSelected', this._setRootFolder); | ||||
| @@ -108,6 +108,7 @@ define( | ||||
|             _setRootFolder: function (options) { | ||||
|                 App.vent.trigger(App.Commands.CloseModalCommand); | ||||
|                 this.ui.rootFolder.val(options.model.id); | ||||
|                 this._rootFolderChanged(); | ||||
|             }, | ||||
|  | ||||
|             _addSeries: function () { | ||||
|   | ||||
| @@ -39,8 +39,6 @@ | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| .page-toolbar { | ||||
|   margin-top    : 10px; | ||||
|   margin-bottom : 30px; | ||||
| @@ -78,8 +76,6 @@ th { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| a, .btn { | ||||
|   i { | ||||
|     cursor : pointer; | ||||
| @@ -91,7 +87,6 @@ a, .btn { | ||||
|   background-color : white; | ||||
| } | ||||
|  | ||||
|  | ||||
| body { | ||||
|   background-color : #1c1c1c; | ||||
|   background-image : url('../Content/Images/pattern.png'); | ||||
| @@ -146,3 +141,9 @@ footer { | ||||
|   background-color : transparent; | ||||
|   box-shadow       : none; | ||||
| } | ||||
|  | ||||
| .validation-errors { | ||||
|   i { | ||||
|     padding-right : 5px; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -12,36 +12,48 @@ define( | ||||
|             var originalOnClose = this.prototype.onClose; | ||||
|             var originalBeforeClose = this.prototype.onBeforeClose; | ||||
|  | ||||
|             var errorHandler = function (response) { | ||||
|  | ||||
|                 if (response.status === 400) { | ||||
|  | ||||
|                     var view = this; | ||||
|                     var validationErrors = JSON.parse(response.responseText); | ||||
|                     _.each(validationErrors, function (error) { | ||||
|                         view.$el.processServerError(error); | ||||
|                     }); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|  | ||||
|             var validatedSync = function (method, model,options) { | ||||
|                 this.$el.removeAllErrors(); | ||||
|                 arguments[2].isValidatedCall = true; | ||||
|                 return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this)); | ||||
|             }; | ||||
|  | ||||
|             var bindToModel = function (model) { | ||||
|  | ||||
|                 if (!model._originalSync) { | ||||
|                     model._originalSync = model.sync; | ||||
|                     model.sync = validatedSync.bind(this); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             this.prototype.onRender = function () { | ||||
|  | ||||
|                 Validation.bind(this); | ||||
|  | ||||
|  | ||||
|                 if (!this.originalSync && this.model) { | ||||
|  | ||||
|                     var self = this; | ||||
|                     this.originalSync = this.model.sync; | ||||
|  | ||||
|  | ||||
|                     var boundHandler = errorHandler.bind(this); | ||||
|  | ||||
|                     this.model.sync = function () { | ||||
|                         self.$el.removeAllErrors(); | ||||
|  | ||||
|                         arguments[2].isValidatedCall = true; | ||||
|  | ||||
|                         return self.originalSync.apply(this, arguments).fail(boundHandler); | ||||
|                     }; | ||||
|                 } | ||||
|                 this.bindToModelValidation = bindToModel.bind(this); | ||||
|  | ||||
|                 if (this.model) { | ||||
|                     if (originalOnRender) { | ||||
|                         originalOnRender.call(this); | ||||
|                     } | ||||
|                     this.bindToModelValidation(this.model); | ||||
|                 } | ||||
|  | ||||
|                 if (originalOnRender) { | ||||
|                     originalOnRender.call(this); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|  | ||||
|             this.prototype.onBeforeClose = function () { | ||||
|  | ||||
|                 if (this.model) { | ||||
| @@ -65,22 +77,6 @@ define( | ||||
|             }; | ||||
|  | ||||
|  | ||||
|             var errorHandler = function (response) { | ||||
|  | ||||
|                 if (response.status === 400) { | ||||
|  | ||||
|                     var view = this; | ||||
|  | ||||
|                     var validationErrors = JSON.parse(response.responseText); | ||||
|  | ||||
|                     _.each(validationErrors, function (error) { | ||||
|                         view.$el.processServerError(error); | ||||
|                     }); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|  | ||||
|  | ||||
|             return this; | ||||
|         }; | ||||
|     }); | ||||
|   | ||||
| @@ -8,6 +8,10 @@ define( | ||||
|  | ||||
|             var validationName = error.propertyName.toLowerCase(); | ||||
|  | ||||
|             this.find('.validation-errors') | ||||
|                 .addClass('alert alert-error') | ||||
|                 .append('<div><i class="icon-exclamation-sign"></i>' + error.errorMessage + '</div>'); | ||||
|  | ||||
|             var input = this.find('[name]').filter(function () { | ||||
|                 return this.name.toLowerCase() === validationName; | ||||
|             }); | ||||
| @@ -40,11 +44,12 @@ define( | ||||
|         }; | ||||
|  | ||||
|         $.fn.addFormError = function (error) { | ||||
|             this.find('.control-group').parent().prepend('<div class="alert alert-error validation-error">'+ error.errorMessage +'</div>') | ||||
|             this.find('.control-group').parent().prepend('<div class="alert alert-error validation-error">' + error.errorMessage + '</div>') | ||||
|         }; | ||||
|  | ||||
|         $.fn.removeAllErrors = function () { | ||||
|             this.find('.error').removeClass('error'); | ||||
|             this.find('.validation-errors').removeClass('alert').removeClass('alert-error').html(''); | ||||
|             this.find('.validation-error').remove(); | ||||
|             return this.find('.help-inline.error-message').remove(); | ||||
|         }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user