added path validation to add series/ recent folders.
This commit is contained in:
parent
bf9946b653
commit
4465d50a31
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue