added path validation to add series/ recent folders.

This commit is contained in:
kay.one 2013-08-31 13:31:58 -07:00
parent bf9946b653
commit 4465d50a31
19 changed files with 146 additions and 88 deletions

View File

@ -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>

View File

@ -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)

View File

@ -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();
}

View 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();
}
}
}

View File

@ -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());
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;

View File

@ -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");
}
}

View File

@ -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();

View File

@ -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();
}
}
}

View File

@ -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()
{

View File

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

View File

@ -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);
});

View File

@ -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">&nbsp;<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>

View File

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

View File

@ -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 () {

View File

@ -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;
}
}

View File

@ -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;
};
});

View File

@ -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();
};