added path validation to add series/ recent folders.

pull/29/merge
kay.one 11 years ago
parent bf9946b653
commit 4465d50a31

@ -164,6 +164,7 @@
<Compile Include="System\SystemModule.cs" /> <Compile Include="System\SystemModule.cs" />
<Compile Include="TinyIoCNancyBootstrapper.cs" /> <Compile Include="TinyIoCNancyBootstrapper.cs" />
<Compile Include="Update\UpdateModule.cs" /> <Compile Include="Update\UpdateModule.cs" />
<Compile Include="Validation\PathValidator.cs" />
<Compile Include="Validation\RuleBuilderExtensions.cs" /> <Compile Include="Validation\RuleBuilderExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Api.Mapping; using NzbDrone.Api.Mapping;
using NzbDrone.Api.Validation;
namespace NzbDrone.Api.RootFolders namespace NzbDrone.Api.RootFolders
{ {
@ -17,6 +18,8 @@ namespace NzbDrone.Api.RootFolders
GetResourceById = GetRootFolder; GetResourceById = GetRootFolder;
CreateResource = CreateRootFolder; CreateResource = CreateRootFolder;
DeleteResource = DeleteFolder; DeleteResource = DeleteFolder;
SharedValidator.RuleFor(c=>c.Path).IsValidPath();
} }
private RootFolderResource GetRootFolder(int id) private RootFolderResource GetRootFolder(int id)

@ -31,10 +31,10 @@ namespace NzbDrone.Api.Series
SharedValidator.RuleFor(s => s.QualityProfileId).ValidId(); 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.Path).IsValidPath().When(s => String.IsNullOrEmpty(s.RootFolderPath));
PostValidator.RuleFor(s => s.RootFolderPath).NotEmpty().When(s => String.IsNullOrEmpty(s.Path)); PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path));
PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.Title).NotEmpty();
} }

@ -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;
using FluentValidation.Validators; 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://"); 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; return param;
} }
private static readonly Regex windowsInvalidPathRegex = new Regex(@"[/*<>""|]", RegexOptions.Compiled);
private static readonly Regex windowsPathRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled);
[DebuggerStepThrough] [DebuggerStepThrough]
public static Param<string> IsValidPath(this Param<string> param) public static Param<string> IsValidPath(this Param<string> param)
@ -104,31 +103,14 @@ namespace NzbDrone.Common.EnsureThat
if (string.IsNullOrWhiteSpace(param.Value)) if (string.IsNullOrWhiteSpace(param.Value))
throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace); throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace);
if (param.Value.IsPathValid()) return param;
if (OsInfo.IsLinux) 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));
}
} }
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;
} }
} }
} }

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -45,6 +46,32 @@ namespace NzbDrone.Common
return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), StringComparison.InvariantCultureIgnoreCase); 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) public static bool ContainsInvalidPathChars(this string text)
{ {
return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0; 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) 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"); 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.Config;
using NLog.Targets; using NLog.Targets;
using NUnit.Framework; using NUnit.Framework;
@ -39,7 +40,7 @@ namespace NzbDrone.Integration.Test
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget)); LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget));
} }
[SetUp] [TestFixtureSetUp]
public void SmokeTestSetup() public void SmokeTestSetup()
{ {
_runner = new NzbDroneRunner(); _runner = new NzbDroneRunner();
@ -63,7 +64,7 @@ namespace NzbDrone.Integration.Test
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming"); NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming");
} }
[TearDown] [TestFixtureTearDown]
public void SmokeTestTearDown() public void SmokeTestTearDown()
{ {
_runner.KillAll(); _runner.KillAll();

@ -58,5 +58,17 @@ namespace NzbDrone.Integration.Test
RootFolders.All().Should().BeEmpty(); 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] [TestFixture]
public class SeriesIntegrationTest : IntegrationTest public class SeriesIntegrationTest : IntegrationTest
{ {
[Test]
public void should_have_no_series_on_start_application()
{
Series.All().Should().BeEmpty();
}
[Test] [Test]
public void series_lookup_on_trakt() public void series_lookup_on_trakt()
{ {

@ -1,4 +1,4 @@
'use strict'; 'use strict';
define( define(
[ [
'backbone', 'backbone',

@ -7,10 +7,11 @@ define(
'AddSeries/RootFolders/Collection', 'AddSeries/RootFolders/Collection',
'AddSeries/RootFolders/Model', 'AddSeries/RootFolders/Model',
'Shared/LoadingView', 'Shared/LoadingView',
'Mixins/AsValidatedView',
'Mixins/AutoComplete' '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', template: 'AddSeries/RootFolders/LayoutTemplate',
ui: { ui: {
@ -55,12 +56,16 @@ define(
Path: this.ui.pathInput.val() Path: this.ui.pathInput.val()
}); });
RootFolderCollection.add(newDir); this.bindToModelValidation(newDir);
newDir.save().done(function () { newDir.save().done(function () {
RootFolderCollection.add(newDir);
self.trigger('folderSelected', {model: newDir}); self.trigger('folderSelected', {model: newDir});
}); });
} }
}); });
return AsValidatedView.apply(layout);
}); });

@ -3,10 +3,13 @@
<h3>Select Folder</h3> <h3>Select Folder</h3>
</div> </div>
<div class="modal-body root-folders-modal"> <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">&nbsp;<i class="icon-folder-open"></i></span> <span class="add-on">&nbsp;<i class="icon-folder-open"></i></span>
<input class="span9" type="text" placeholder="Start Typing Folder Path..."> <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> <button class="btn btn-success x-add">
<i class="icon-ok"/>
</button>
</div> </div>
{{#if items}} {{#if items}}
<h4>Recent Folders</h4> <h4>Recent Folders</h4>

@ -4,6 +4,7 @@ define(
'backbone' 'backbone'
], function (Backbone) { ], function (Backbone) {
return Backbone.Model.extend({ return Backbone.Model.extend({
urlRoot : window.ApiRoot + '/rootfolder',
defaults: { defaults: {
freeSpace: 0 freeSpace: 0
} }

@ -38,7 +38,7 @@ define(
this.listenTo(App.vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); this.listenTo(App.vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated);
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
this.listenTo(RootFolders, 'change', this.render); this.listenTo(RootFolders, 'all', this.render);
this.rootFolderLayout = new RootFolderLayout(); this.rootFolderLayout = new RootFolderLayout();
this.listenTo(this.rootFolderLayout, 'folderSelected', this._setRootFolder); this.listenTo(this.rootFolderLayout, 'folderSelected', this._setRootFolder);
@ -108,6 +108,7 @@ define(
_setRootFolder: function (options) { _setRootFolder: function (options) {
App.vent.trigger(App.Commands.CloseModalCommand); App.vent.trigger(App.Commands.CloseModalCommand);
this.ui.rootFolder.val(options.model.id); this.ui.rootFolder.val(options.model.id);
this._rootFolderChanged();
}, },
_addSeries: function () { _addSeries: function () {

@ -39,8 +39,6 @@
} }
} }
.page-toolbar { .page-toolbar {
margin-top : 10px; margin-top : 10px;
margin-bottom : 30px; margin-bottom : 30px;
@ -78,8 +76,6 @@ th {
} }
} }
a, .btn { a, .btn {
i { i {
cursor : pointer; cursor : pointer;
@ -91,7 +87,6 @@ a, .btn {
background-color : white; background-color : white;
} }
body { body {
background-color : #1c1c1c; background-color : #1c1c1c;
background-image : url('../Content/Images/pattern.png'); background-image : url('../Content/Images/pattern.png');
@ -146,3 +141,9 @@ footer {
background-color : transparent; background-color : transparent;
box-shadow : none; box-shadow : none;
} }
.validation-errors {
i {
padding-right : 5px;
}
}

@ -12,36 +12,48 @@ define(
var originalOnClose = this.prototype.onClose; var originalOnClose = this.prototype.onClose;
var originalBeforeClose = this.prototype.onBeforeClose; var originalBeforeClose = this.prototype.onBeforeClose;
var errorHandler = function (response) {
this.prototype.onRender = function () { if (response.status === 400) {
Validation.bind(this);
var view = this;
var validationErrors = JSON.parse(response.responseText);
_.each(validationErrors, function (error) {
view.$el.processServerError(error);
});
}
};
if (!this.originalSync && this.model) {
var self = this; var validatedSync = function (method, model,options) {
this.originalSync = this.model.sync; this.$el.removeAllErrors();
arguments[2].isValidatedCall = true;
return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this));
};
var bindToModel = function (model) {
var boundHandler = errorHandler.bind(this); if (!model._originalSync) {
model._originalSync = model.sync;
model.sync = validatedSync.bind(this);
}
};
this.model.sync = function () { this.prototype.onRender = function () {
self.$el.removeAllErrors();
arguments[2].isValidatedCall = true; Validation.bind(this);
this.bindToModelValidation = bindToModel.bind(this);
return self.originalSync.apply(this, arguments).fail(boundHandler); if (this.model) {
}; this.bindToModelValidation(this.model);
} }
if (this.model) { if (originalOnRender) {
if (originalOnRender) { originalOnRender.call(this);
originalOnRender.call(this);
}
} }
}; };
this.prototype.onBeforeClose = function () { this.prototype.onBeforeClose = function () {
if (this.model) { 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; return this;
}; };
}); });

@ -8,6 +8,10 @@ define(
var validationName = error.propertyName.toLowerCase(); 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 () { var input = this.find('[name]').filter(function () {
return this.name.toLowerCase() === validationName; return this.name.toLowerCase() === validationName;
}); });
@ -40,11 +44,12 @@ define(
}; };
$.fn.addFormError = function (error) { $.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 () { $.fn.removeAllErrors = function () {
this.find('.error').removeClass('error'); this.find('.error').removeClass('error');
this.find('.validation-errors').removeClass('alert').removeClass('alert-error').html('');
this.find('.validation-error').remove(); this.find('.validation-error').remove();
return this.find('.help-inline.error-message').remove(); return this.find('.help-inline.error-message').remove();
}; };

Loading…
Cancel
Save