Centralizes logic to validate complex editors, starts transitioning to new JSON error messages for complex editors, starts adding tests
This commit is contained in:
@@ -3,8 +3,12 @@ using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
// TODO: Rename this, we don't want to use the name "Helper"
|
||||
// TODO: What is this? This requires code docs
|
||||
// TODO: This is not used publicly at all - therefore it probably shouldn't be public
|
||||
public interface IBlockEditorDataHelper
|
||||
{
|
||||
// TODO: Does this abstraction need a reference to JObject? Maybe it does but ideally it doesn't
|
||||
IEnumerable<IBlockReference> GetBlockReferences(JObject layout);
|
||||
bool IsEditorSpecificPropertyKey(string propertyKey);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
@@ -15,19 +13,73 @@ namespace Umbraco.Core.Services
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizedTextService _textService;
|
||||
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_textService = textService;
|
||||
}
|
||||
|
||||
//TODO: Remove this method in favor of the overload specifying all dependencies
|
||||
public PropertyValidationService()
|
||||
: this(Current.PropertyEditors, Current.Services.DataTypeService)
|
||||
: this(Current.PropertyEditors, Current.Services.DataTypeService, Current.Services.TextService)
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> ValidatePropertyValue(
|
||||
PropertyType propertyType,
|
||||
object postedValue)
|
||||
{
|
||||
if (propertyType is null) throw new ArgumentNullException(nameof(propertyType));
|
||||
var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId);
|
||||
if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId);
|
||||
|
||||
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
|
||||
if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias); ;
|
||||
|
||||
return ValidatePropertyValue(_textService, editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage);
|
||||
}
|
||||
|
||||
internal static IEnumerable<ValidationResult> ValidatePropertyValue(
|
||||
ILocalizedTextService textService,
|
||||
IDataEditor editor,
|
||||
IDataType dataType,
|
||||
object postedValue,
|
||||
bool isRequired,
|
||||
string validationRegExp,
|
||||
string isRequiredMessage,
|
||||
string validationRegExpMessage)
|
||||
{
|
||||
// Retrieve default messages used for required and regex validatation. We'll replace these
|
||||
// if set with custom ones if they've been provided for a given property.
|
||||
var requiredDefaultMessages = new[]
|
||||
{
|
||||
textService.Localize("validation", "invalidNull"),
|
||||
textService.Localize("validation", "invalidEmpty")
|
||||
};
|
||||
var formatDefaultMessages = new[]
|
||||
{
|
||||
textService.Localize("validation", "invalidPattern"),
|
||||
};
|
||||
|
||||
var valueEditor = editor.GetValueEditor(dataType.Configuration);
|
||||
foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp))
|
||||
{
|
||||
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
|
||||
if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
validationResult.ErrorMessage = isRequiredMessage;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
validationResult.ErrorMessage = validationRegExpMessage;
|
||||
}
|
||||
yield return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the content item's properties pass validation rules
|
||||
/// </summary>
|
||||
|
||||
@@ -442,7 +442,10 @@ namespace Umbraco.Tests.Models
|
||||
[Test]
|
||||
public void ContentPublishValuesWithMixedPropertyTypeVariations()
|
||||
{
|
||||
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(
|
||||
Current.Factory.GetInstance<PropertyEditorCollection>(),
|
||||
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
|
||||
Current.Factory.GetInstance<ServiceContext>().TextService);
|
||||
const string langFr = "fr-FR";
|
||||
|
||||
// content type varies by Culture
|
||||
@@ -574,7 +577,10 @@ namespace Umbraco.Tests.Models
|
||||
prop.SetValue("a");
|
||||
Assert.AreEqual("a", prop.GetValue());
|
||||
Assert.IsNull(prop.GetValue(published: true));
|
||||
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(
|
||||
Current.Factory.GetInstance<PropertyEditorCollection>(),
|
||||
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
|
||||
Current.Factory.GetInstance<ServiceContext>().TextService);
|
||||
|
||||
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
|
||||
|
||||
|
||||
@@ -1197,7 +1197,7 @@ namespace Umbraco.Tests.Services
|
||||
Assert.IsFalse(content.HasIdentity);
|
||||
|
||||
// content cannot publish values because they are invalid
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService, ServiceContext.TextService);
|
||||
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant);
|
||||
Assert.IsFalse(isValid);
|
||||
Assert.IsNotEmpty(invalidProperties);
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
var propEditors = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
|
||||
|
||||
validationService = new PropertyValidationService(propEditors, dataTypeService.Object);
|
||||
validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of<ILocalizedTextService>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -41,12 +41,12 @@ namespace Umbraco.Tests.TestHelpers.Entities
|
||||
};
|
||||
|
||||
var contentCollection = new PropertyTypeCollection(true);
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -87 });
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.RichtextEditor });
|
||||
|
||||
var metaCollection = new PropertyTypeCollection(true);
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -89 });
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.Textarea });
|
||||
|
||||
contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 });
|
||||
contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 });
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
<Compile Include="Web\HttpCookieExtensionsTests.cs" />
|
||||
<Compile Include="Templates\HtmlImageSourceParserTests.cs" />
|
||||
<Compile Include="Web\Mvc\HtmlStringUtilitiesTests.cs" />
|
||||
<Compile Include="Web\ModelStateExtensionsTests.cs" />
|
||||
<Compile Include="Web\Validation\ModelStateExtensionsTests.cs" />
|
||||
<Compile Include="Web\Mvc\RenderIndexActionSelectorAttributeTests.cs" />
|
||||
<Compile Include="Persistence\NPocoTests\PetaPocoCachesTest.cs" />
|
||||
<Compile Include="Persistence\Repositories\AuditRepositoryTest.cs" />
|
||||
@@ -513,6 +513,7 @@
|
||||
<Compile Include="CoreThings\VersionExtensionTests.cs" />
|
||||
<Compile Include="Web\TemplateUtilitiesTests.cs" />
|
||||
<Compile Include="Web\UrlHelperExtensionTests.cs" />
|
||||
<Compile Include="Web\Validation\ContentModelValidatorTests.cs" />
|
||||
<Compile Include="Web\WebExtensionMethodTests.cs" />
|
||||
<Compile Include="CoreThings\XmlExtensionsTests.cs" />
|
||||
<Compile Include="Misc\XmlHelperTests.cs" />
|
||||
|
||||
254
src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs
Normal file
254
src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.Editors.Filters;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.Editors.Binders;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Tests.Testing;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Composing;
|
||||
using System.Web.Http.ModelBinding;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Umbraco.Tests.Web.Validation
|
||||
{
|
||||
[UmbracoTest(Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)]
|
||||
[TestFixture]
|
||||
public class ContentModelValidatorTests : UmbracoTestBase
|
||||
{
|
||||
private const int ComplexDataTypeId = 9999;
|
||||
private const string ContentTypeAlias = "textPage";
|
||||
private ContentType _contentType;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
base.SetUp();
|
||||
|
||||
_contentType = MockedContentTypes.CreateTextPageContentType(ContentTypeAlias);
|
||||
// add complex editor
|
||||
_contentType.AddPropertyType(
|
||||
new PropertyType("complexTest", ValueStorageType.Ntext) { Alias = "complex", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = ComplexDataTypeId },
|
||||
"Content");
|
||||
|
||||
// make them all validate with a regex rule that will not pass
|
||||
foreach (var prop in _contentType.PropertyTypes)
|
||||
{
|
||||
prop.ValidationRegExp = "^donotmatch$";
|
||||
prop.ValidationRegExpMessage = "Does not match!";
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Compose()
|
||||
{
|
||||
base.Compose();
|
||||
|
||||
var complexEditorConfig = new NestedContentConfiguration
|
||||
{
|
||||
ContentTypes = new[]
|
||||
{
|
||||
new NestedContentConfiguration.ContentType { Alias = "feature" }
|
||||
}
|
||||
};
|
||||
var dataTypeService = new Mock<IDataTypeService>();
|
||||
dataTypeService.Setup(x => x.GetDataType(It.IsAny<int>()))
|
||||
.Returns((int id) => id == ComplexDataTypeId
|
||||
? Mock.Of<IDataType>(x => x.Configuration == complexEditorConfig)
|
||||
: Mock.Of<IDataType>());
|
||||
|
||||
var contentTypeService = new Mock<IContentTypeService>();
|
||||
contentTypeService.Setup(x => x.GetAll(It.IsAny<int[]>()))
|
||||
.Returns(() => new List<IContentType>
|
||||
{
|
||||
_contentType
|
||||
});
|
||||
|
||||
var textService = new Mock<ILocalizedTextService>();
|
||||
textService.Setup(x => x.Localize("validation/invalidPattern", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns(() => "invalidPattern");
|
||||
textService.Setup(x => x.Localize("validation/invalidNull", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidNull");
|
||||
textService.Setup(x => x.Localize("validation/invalidEmpty", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidEmpty");
|
||||
|
||||
Composition.RegisterUnique(x => Mock.Of<IDataTypeService>(x => x.GetDataType(It.IsAny<int>()) == Mock.Of<IDataType>()));
|
||||
Composition.RegisterUnique(x => dataTypeService.Object);
|
||||
Composition.RegisterUnique(x => contentTypeService.Object);
|
||||
Composition.RegisterUnique(x => textService.Object);
|
||||
|
||||
Composition.WithCollectionBuilder<DataEditorCollectionBuilder>()
|
||||
.Add<TestEditor>()
|
||||
.Add<ComplexTestEditor>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test()
|
||||
{
|
||||
var validator = new ContentSaveModelValidator(
|
||||
Factory.GetInstance<ILogger>(),
|
||||
Mock.Of<IUmbracoContextAccessor>(),
|
||||
Factory.GetInstance<ILocalizedTextService>());
|
||||
|
||||
var content = MockedContent.CreateTextpageContent(_contentType, "test", -1);
|
||||
|
||||
const string complexValue = @"[{
|
||||
""key"": ""c8df5136-d606-41f0-9134-dea6ae0c2fd9"",
|
||||
""name"": ""Hello world"",
|
||||
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
|
||||
""title"": ""Hello world""
|
||||
}, {
|
||||
""key"": ""f916104a-4082-48b2-a515-5c4bf2230f38"",
|
||||
""name"": ""Hello worldsss ddf"",
|
||||
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
|
||||
""title"": ""Hello worldsss ddf""
|
||||
}
|
||||
]";
|
||||
content.SetValue("complex", complexValue);
|
||||
|
||||
// map the persisted properties to a model representing properties to save
|
||||
//var saveProperties = content.Properties.Select(x => Mapper.Map<ContentPropertyBasic>(x)).ToList();
|
||||
var saveProperties = content.Properties.Select(x =>
|
||||
{
|
||||
return new ContentPropertyBasic
|
||||
{
|
||||
Alias = x.Alias,
|
||||
Id = x.Id,
|
||||
Value = x.GetValue()
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
var saveVariants = new List<ContentVariantSave>
|
||||
{
|
||||
new ContentVariantSave
|
||||
{
|
||||
Culture = string.Empty,
|
||||
Segment = string.Empty,
|
||||
Name = content.Name,
|
||||
Save = true,
|
||||
Properties = saveProperties
|
||||
}
|
||||
};
|
||||
|
||||
var save = new ContentItemSave
|
||||
{
|
||||
Id = content.Id,
|
||||
Action = ContentSaveAction.Save,
|
||||
ContentTypeAlias = _contentType.Alias,
|
||||
ParentId = -1,
|
||||
PersistedContent = content,
|
||||
TemplateAlias = null,
|
||||
Variants = saveVariants
|
||||
};
|
||||
|
||||
// This will map the ContentItemSave.Variants.PropertyCollectionDto and then map the values in the saved model
|
||||
// back onto the persisted IContent model.
|
||||
ContentItemBinder.BindModel(save, content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var isValid = validator.ValidatePropertiesData(save, saveVariants[0], saveVariants[0].PropertyCollectionDto, modelState);
|
||||
|
||||
// list results for debugging
|
||||
foreach (var state in modelState)
|
||||
{
|
||||
Console.WriteLine(state.Key);
|
||||
foreach (var error in state.Value.Errors)
|
||||
{
|
||||
Console.WriteLine("\t" + error.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// assert
|
||||
|
||||
Assert.IsFalse(isValid);
|
||||
Assert.AreEqual(5, modelState.Keys.Count);
|
||||
const string complexPropertyKey = "_Properties.complex.invariant.null";
|
||||
Assert.IsTrue(modelState.Keys.Contains(complexPropertyKey));
|
||||
foreach(var state in modelState.Where(x => x.Key != complexPropertyKey))
|
||||
{
|
||||
foreach (var error in state.Value.Errors)
|
||||
{
|
||||
Assert.IsTrue(error.ErrorMessage.DetectIsJson());
|
||||
var json = JsonConvert.DeserializeObject<JObject>(error.ErrorMessage);
|
||||
Assert.IsNotEmpty(json["errorMessage"].Value<string>());
|
||||
Assert.AreEqual(1, json["memberNames"].Value<JArray>().Count);
|
||||
}
|
||||
}
|
||||
var complexEditorErrors = modelState.Single(x => x.Key == complexPropertyKey).Value.Errors;
|
||||
Assert.AreEqual(3, complexEditorErrors.Count);
|
||||
var nestedError = complexEditorErrors.Single(x => x.ErrorMessage.Contains("nestedValidation"));
|
||||
var jsonNestedError = JsonConvert.DeserializeObject<JObject>(nestedError.ErrorMessage);
|
||||
Assert.AreEqual(JTokenType.Array, jsonNestedError["nestedValidation"].Type);
|
||||
var nestedValidation = (JArray)jsonNestedError["nestedValidation"];
|
||||
Assert.AreEqual(2, nestedValidation.Count); // there are 2 because there are 2 nested content rows
|
||||
foreach(var rowErrors in nestedValidation)
|
||||
{
|
||||
var elementTypeErrors = (JArray)rowErrors; // this is an array of errors for the nested content row (element type)
|
||||
Assert.AreEqual(2, elementTypeErrors.Count);
|
||||
foreach(var elementTypeErr in elementTypeErrors)
|
||||
{
|
||||
Assert.IsNotEmpty(elementTypeErr["errorMessage"].Value<string>());
|
||||
Assert.AreEqual(1, elementTypeErr["memberNames"].Value<JArray>().Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HideFromTypeFinder]
|
||||
[DataEditor("complexTest", "test", "test")]
|
||||
public class ComplexTestEditor : NestedContentPropertyEditor
|
||||
{
|
||||
public ComplexTestEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(logger, propertyEditors, dataTypeService, contentTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor()
|
||||
{
|
||||
var editor = base.CreateValueEditor();
|
||||
editor.Validators.Add(new NeverValidateValidator());
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
||||
[HideFromTypeFinder]
|
||||
[DataEditor("test", "test", "test")] // This alias aligns with the prop editor alias for all properties created from MockedContentTypes.CreateTextPageContentType
|
||||
public class TestEditor : DataEditor
|
||||
{
|
||||
public TestEditor(ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor() => new TestValueEditor(Attribute);
|
||||
|
||||
private class TestValueEditor : DataValueEditor
|
||||
{
|
||||
public TestValueEditor(DataEditorAttribute attribute)
|
||||
: base(attribute)
|
||||
{
|
||||
Validators.Add(new NeverValidateValidator());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class NeverValidateValidator : IValueValidator
|
||||
{
|
||||
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
|
||||
{
|
||||
yield return new ValidationResult("WRONG!", new[] { "innerFieldId" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using NUnit.Framework;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.Tests.Web
|
||||
namespace Umbraco.Tests.Web.Validation
|
||||
{
|
||||
[TestFixture]
|
||||
public class ModelStateExtensionsTests
|
||||
@@ -11,8 +11,8 @@ namespace Umbraco.Web.Editors.Binders
|
||||
{
|
||||
}
|
||||
|
||||
public BlueprintItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
: base(logger, services, umbracoContextAccessor)
|
||||
public BlueprintItemBinder(ServiceContext services)
|
||||
: base(services)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,13 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// </summary>
|
||||
internal class ContentItemBinder : IModelBinder
|
||||
{
|
||||
private readonly ContentModelBinderHelper _modelBinderHelper;
|
||||
|
||||
public ContentItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
|
||||
public ContentItemBinder() : this(Current.Services)
|
||||
{
|
||||
}
|
||||
|
||||
public ContentItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
public ContentItemBinder(ServiceContext services)
|
||||
{
|
||||
Services = services;
|
||||
_modelBinderHelper = new ContentModelBinderHelper();
|
||||
}
|
||||
|
||||
protected ServiceContext Services { get; }
|
||||
@@ -39,10 +36,20 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// <returns></returns>
|
||||
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
|
||||
{
|
||||
var model = _modelBinderHelper.BindModelFromMultipartRequest<ContentItemSave>(actionContext, bindingContext);
|
||||
var model = ContentModelBinderHelper.BindModelFromMultipartRequest<ContentItemSave>(actionContext, bindingContext);
|
||||
if (model == null) return false;
|
||||
|
||||
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
|
||||
BindModel(model, ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void BindModel(ContentItemSave model, IContent persistedContent)
|
||||
{
|
||||
if (model is null) throw new ArgumentNullException(nameof(model));
|
||||
if (persistedContent is null) throw new ArgumentNullException(nameof(persistedContent));
|
||||
|
||||
model.PersistedContent = persistedContent;
|
||||
|
||||
//create the dto from the persisted model
|
||||
if (model.PersistedContent != null)
|
||||
@@ -60,11 +67,9 @@ namespace Umbraco.Web.Editors.Binders
|
||||
});
|
||||
|
||||
//now map all of the saved values to the dto
|
||||
_modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto);
|
||||
ContentModelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual IContent GetExisting(ContentItemSave model)
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// <summary>
|
||||
/// Helper methods to bind media/member models
|
||||
/// </summary>
|
||||
internal class ContentModelBinderHelper
|
||||
internal static class ContentModelBinderHelper
|
||||
{
|
||||
public TModelSave BindModelFromMultipartRequest<TModelSave>(HttpActionContext actionContext, ModelBindingContext bindingContext)
|
||||
public static TModelSave BindModelFromMultipartRequest<TModelSave>(HttpActionContext actionContext, ModelBindingContext bindingContext)
|
||||
where TModelSave : IHaveUploadedFiles
|
||||
{
|
||||
var result = actionContext.ReadAsMultipart(SystemDirectories.TempFileUploads);
|
||||
@@ -86,7 +86,7 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// </summary>
|
||||
/// <param name="saveModel"></param>
|
||||
/// <param name="dto"></param>
|
||||
public void MapPropertyValuesFromSaved(IContentProperties<ContentPropertyBasic> saveModel, ContentPropertyCollectionDto dto)
|
||||
public static void MapPropertyValuesFromSaved(IContentProperties<ContentPropertyBasic> saveModel, ContentPropertyCollectionDto dto)
|
||||
{
|
||||
//NOTE: Don't convert this to linq, this is much quicker
|
||||
foreach (var p in saveModel.Properties)
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// </summary>
|
||||
internal class MediaItemBinder : IModelBinder
|
||||
{
|
||||
private readonly ContentModelBinderHelper _modelBinderHelper;
|
||||
private readonly ServiceContext _services;
|
||||
|
||||
public MediaItemBinder() : this(Current.Services)
|
||||
@@ -23,7 +22,6 @@ namespace Umbraco.Web.Editors.Binders
|
||||
public MediaItemBinder(ServiceContext services)
|
||||
{
|
||||
_services = services;
|
||||
_modelBinderHelper = new ContentModelBinderHelper();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -34,7 +32,7 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// <returns></returns>
|
||||
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
|
||||
{
|
||||
var model = _modelBinderHelper.BindModelFromMultipartRequest<MediaItemSave>(actionContext, bindingContext);
|
||||
var model = ContentModelBinderHelper.BindModelFromMultipartRequest<MediaItemSave>(actionContext, bindingContext);
|
||||
if (model == null) return false;
|
||||
|
||||
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
|
||||
@@ -44,7 +42,7 @@ namespace Umbraco.Web.Editors.Binders
|
||||
{
|
||||
model.PropertyCollectionDto = Current.Mapper.Map<IMedia, ContentPropertyCollectionDto>(model.PersistedContent);
|
||||
//now map all of the saved values to the dto
|
||||
_modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
|
||||
ContentModelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
|
||||
}
|
||||
|
||||
model.Name = model.Name.Trim();
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// </summary>
|
||||
internal class MemberBinder : IModelBinder
|
||||
{
|
||||
private readonly ContentModelBinderHelper _modelBinderHelper;
|
||||
private readonly ServiceContext _services;
|
||||
|
||||
public MemberBinder() : this(Current.Services)
|
||||
@@ -30,7 +29,6 @@ namespace Umbraco.Web.Editors.Binders
|
||||
public MemberBinder(ServiceContext services)
|
||||
{
|
||||
_services = services;
|
||||
_modelBinderHelper = new ContentModelBinderHelper();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,7 +39,7 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// <returns></returns>
|
||||
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
|
||||
{
|
||||
var model = _modelBinderHelper.BindModelFromMultipartRequest<MemberSave>(actionContext, bindingContext);
|
||||
var model = ContentModelBinderHelper.BindModelFromMultipartRequest<MemberSave>(actionContext, bindingContext);
|
||||
if (model == null) return false;
|
||||
|
||||
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
|
||||
@@ -51,7 +49,7 @@ namespace Umbraco.Web.Editors.Binders
|
||||
{
|
||||
model.PropertyCollectionDto = Current.Mapper.Map<IMember, ContentPropertyCollectionDto>(model.PersistedContent);
|
||||
//now map all of the saved values to the dto
|
||||
_modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
|
||||
ContentModelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
|
||||
}
|
||||
|
||||
model.Name = model.Name.Trim();
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Web.Http.Controllers;
|
||||
using System.Web.Http.ModelBinding;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Web.Editors.Filters
|
||||
{
|
||||
@@ -125,18 +129,6 @@ namespace Umbraco.Web.Editors.Filters
|
||||
{
|
||||
var properties = modelWithProperties.Properties.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
// Retrieve default messages used for required and regex validatation. We'll replace these
|
||||
// if set with custom ones if they've been provided for a given property.
|
||||
var requiredDefaultMessages = new[]
|
||||
{
|
||||
_textService.Localize("validation", "invalidNull"),
|
||||
_textService.Localize("validation", "invalidEmpty")
|
||||
};
|
||||
var formatDefaultMessages = new[]
|
||||
{
|
||||
_textService.Localize("validation", "invalidPattern"),
|
||||
};
|
||||
|
||||
foreach (var p in dto.Properties)
|
||||
{
|
||||
var editor = p.PropertyEditor;
|
||||
@@ -156,7 +148,7 @@ namespace Umbraco.Web.Editors.Filters
|
||||
|
||||
var postedValue = postedProp.Value;
|
||||
|
||||
ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState, requiredDefaultMessages, formatDefaultMessages);
|
||||
ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState);
|
||||
|
||||
}
|
||||
|
||||
@@ -180,26 +172,29 @@ namespace Umbraco.Web.Editors.Filters
|
||||
IDataEditor editor,
|
||||
ContentPropertyDto property,
|
||||
object postedValue,
|
||||
ModelStateDictionary modelState,
|
||||
string[] requiredDefaultMessages,
|
||||
string[] formatDefaultMessages)
|
||||
ModelStateDictionary modelState)
|
||||
{
|
||||
var valueEditor = editor.GetValueEditor(property.DataType.Configuration);
|
||||
foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.ValidationRegExp))
|
||||
if (property is null) throw new ArgumentNullException(nameof(property));
|
||||
if (property.DataType is null) throw new InvalidOperationException($"{nameof(property)}.{nameof(property.DataType)} cannot be null");
|
||||
|
||||
foreach (var validationResult in PropertyValidationService.ValidatePropertyValue(
|
||||
_textService, editor, property.DataType, postedValue, property.IsRequired,
|
||||
property.ValidationRegExp, property.IsRequiredMessage, property.ValidationRegExpMessage))
|
||||
{
|
||||
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
|
||||
if (property.IsRequired && !string.IsNullOrWhiteSpace(property.IsRequiredMessage) && requiredDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
r.ErrorMessage = property.IsRequiredMessage;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(property.ValidationRegExp) && !string.IsNullOrWhiteSpace(property.ValidationRegExpMessage) && formatDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
r.ErrorMessage = property.ValidationRegExpMessage;
|
||||
}
|
||||
|
||||
modelState.AddPropertyError(r, property.Alias, property.Culture, property.Segment);
|
||||
AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void AddPropertyError(
|
||||
TModelSave model,
|
||||
TModelWithProperties modelWithProperties,
|
||||
IDataEditor editor,
|
||||
ContentPropertyDto property,
|
||||
ValidationResult validationResult,
|
||||
ModelStateDictionary modelState)
|
||||
{
|
||||
modelState.AddPropertyError(validationResult, property.Alias, property.Culture, property.Segment);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Umbraco.Core.Logging;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Web.Http.ModelBinding;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
|
||||
@@ -13,5 +16,30 @@ namespace Umbraco.Web.Editors.Filters
|
||||
public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void AddPropertyError(ContentItemSave model, ContentVariantSave modelWithProperties, IDataEditor editor, ContentPropertyDto property, ValidationResult validationResult, ModelStateDictionary modelState)
|
||||
{
|
||||
// Original idea: See if we can build up the JSON + JSON Path
|
||||
// SD: I'm just keeping these notes here for later just to remind myself that we might want to take into account the tab number in validation
|
||||
// which we might be able to get in the PropertyValidationService anyways?
|
||||
|
||||
// Create a JSON + JSON Path key, see https://gist.github.com/Shazwazza/ad9fcbdb0fdacff1179a9eed88393aa6
|
||||
|
||||
//var json = new PropertyError
|
||||
//{
|
||||
// Culture = property.Culture,
|
||||
// Segment = property.Segment
|
||||
//};
|
||||
|
||||
// TODO: Hrm, we can't get the tab index without a reference to the content type itself! the IContent doesn't contain a reference to groups/indexes
|
||||
// BUT! I think it contains a reference to the group alias so we could use JSON Path for a group alias instead of index like:
|
||||
// .tabs[?(@.alias=='Content')]
|
||||
//var tabIndex = ??
|
||||
|
||||
//var jsonPath = "$.variants[0].tabs[0].properties[?(@.alias=='title')].value[0]";
|
||||
|
||||
base.AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@@ -6,6 +7,7 @@ using System.Web.Mvc;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Web
|
||||
{
|
||||
@@ -51,13 +53,26 @@ namespace Umbraco.Web
|
||||
internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
||||
ValidationResult result, string propertyAlias, string culture = "", string segment = "")
|
||||
{
|
||||
if (culture == null)
|
||||
culture = "";
|
||||
modelState.AddValidationError(result, "_Properties", propertyAlias,
|
||||
|
||||
var propValidationResult = new PropertyValidationResult(result);
|
||||
|
||||
var keyParts = new[]
|
||||
{
|
||||
"_Properties",
|
||||
propertyAlias,
|
||||
//if the culture is null, we'll add the term 'invariant' as part of the key
|
||||
culture.IsNullOrWhiteSpace() ? "invariant" : culture,
|
||||
// if the segment is null, we'll add the term 'null' as part of the key
|
||||
segment.IsNullOrWhiteSpace() ? "null" : segment);
|
||||
segment.IsNullOrWhiteSpace() ? "null" : segment
|
||||
};
|
||||
|
||||
var key = string.Join(".", keyParts);
|
||||
|
||||
modelState.AddModelError(
|
||||
key,
|
||||
JsonConvert.SerializeObject(
|
||||
propValidationResult,
|
||||
new ValidationResultConverter()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,5 +251,6 @@ namespace Umbraco.Web
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
[DataContract(Name = "content", Namespace = "")]
|
||||
public class ContentItemSave : IContentSave<IContent>
|
||||
{
|
||||
protected ContentItemSave()
|
||||
public ContentItemSave()
|
||||
{
|
||||
UploadedFiles = new List<ContentPropertyFile>();
|
||||
Variants = new List<ContentVariantSave>();
|
||||
|
||||
@@ -44,6 +44,9 @@ namespace Umbraco.Web.Models.Mapping
|
||||
property.PropertyType.PropertyEditorAlias);
|
||||
|
||||
editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label];
|
||||
|
||||
if (editor == null)
|
||||
throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}");
|
||||
}
|
||||
|
||||
dest.Id = property.Id;
|
||||
|
||||
@@ -86,75 +86,24 @@ namespace Umbraco.Web.PropertyEditors
|
||||
}
|
||||
}
|
||||
|
||||
internal class BlockEditorValidator : IValueValidator
|
||||
internal class BlockEditorValidator : ComplexEditorValidator
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly BlockEditorValues _blockEditorValues;
|
||||
|
||||
public BlockEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, BlockEditorValues blockEditorValues)
|
||||
public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_blockEditorValues = blockEditorValues;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
|
||||
protected override IEnumerable<ElementTypeValidationModel> GetElementsFromValue(object value)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
foreach (var row in _blockEditorValues.GetPropertyValues(rawValue, out _))
|
||||
foreach (var row in _blockEditorValues.GetPropertyValues(value, out _))
|
||||
{
|
||||
if (row.PropType == null) continue;
|
||||
|
||||
var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
|
||||
var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
if (propertyEditor == null) continue;
|
||||
|
||||
foreach (var validator in propertyEditor.GetValueEditor().Validators)
|
||||
{
|
||||
foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config))
|
||||
{
|
||||
result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage;
|
||||
validationResults.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Check mandatory
|
||||
if (row.PropType.Mandatory)
|
||||
{
|
||||
if (row.JsonRowValue[row.PropKey] == null)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
|
||||
? $"'{row.PropType.Name}' cannot be null"
|
||||
: row.PropType.MandatoryMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues))
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
|
||||
? $"'{row.PropType.Name}' cannot be empty"
|
||||
: row.PropType.MandatoryMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
}
|
||||
|
||||
// Check regex
|
||||
if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace()
|
||||
&& row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
var regex = new Regex(row.PropType.ValidationRegExp);
|
||||
if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString()))
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage)
|
||||
? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern"
|
||||
: row.PropType.ValidationRegExpMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
}
|
||||
var val = row.JsonRowValue[row.PropKey];
|
||||
yield return new ElementTypeValidationModel(val, row.PropType);
|
||||
}
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
#region IBlockEditorDataHelper
|
||||
|
||||
// TODO: Rename this we don't want to use the name "Helper"
|
||||
private class DataHelper : IBlockEditorDataHelper
|
||||
{
|
||||
public IEnumerable<IBlockReference> GetBlockReferences(JObject layout)
|
||||
|
||||
73
src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs
Normal file
73
src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to validate complex editors that contain nested editors
|
||||
/// </summary>
|
||||
public abstract class ComplexEditorValidator : IValueValidator
|
||||
{
|
||||
private readonly PropertyValidationService _propertyValidationService;
|
||||
|
||||
public ComplexEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
{
|
||||
_propertyValidationService = new PropertyValidationService(propertyEditors, dataTypeService, textService);
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
|
||||
{
|
||||
var elements = GetElementsFromValue(value);
|
||||
var rowResults = GetNestedValidationResults(elements).ToList();
|
||||
|
||||
return rowResults.Count > 0
|
||||
? new NestedValidationResults(rowResults).Yield()
|
||||
: Enumerable.Empty<ValidationResult>();
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<ElementTypeValidationModel> GetElementsFromValue(object value);
|
||||
|
||||
/// <summary>
|
||||
/// Return a nested validation result per row
|
||||
/// </summary>
|
||||
/// <param name="rawValue"></param>
|
||||
/// <returns></returns>
|
||||
protected IEnumerable<NestedValidationResults> GetNestedValidationResults(IEnumerable<ElementTypeValidationModel> elements)
|
||||
{
|
||||
foreach (var row in elements)
|
||||
{
|
||||
var nestedValidation = new List<ValidationResult>();
|
||||
|
||||
foreach(var validationResult in _propertyValidationService.ValidatePropertyValue(row.PropertyType, row.PostedValue))
|
||||
{
|
||||
nestedValidation.Add(validationResult);
|
||||
}
|
||||
|
||||
if (nestedValidation.Count > 0)
|
||||
{
|
||||
yield return new NestedValidationResults(nestedValidation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ElementTypeValidationModel
|
||||
{
|
||||
public ElementTypeValidationModel(object postedValue, PropertyType propertyType)
|
||||
{
|
||||
PostedValue = postedValue ?? throw new ArgumentNullException(nameof(postedValue));
|
||||
PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
|
||||
}
|
||||
|
||||
public object PostedValue { get; }
|
||||
public PropertyType PropertyType { get; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
@@ -13,9 +14,11 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents a nested content property editor.
|
||||
/// </summary>
|
||||
@@ -31,14 +34,20 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
|
||||
|
||||
[Obsolete("Use the constructor specifying all parameters instead")]
|
||||
public NestedContentPropertyEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService)
|
||||
: this(logger, propertyEditors, dataTypeService, contentTypeService, Current.Services.TextService) { }
|
||||
|
||||
public NestedContentPropertyEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService localizedTextService)
|
||||
: base (logger)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_contentTypeService = contentTypeService;
|
||||
_localizedTextService = localizedTextService;
|
||||
}
|
||||
|
||||
// has to be lazy else circular dep in ctor
|
||||
@@ -52,7 +61,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
#region Value Editor
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService);
|
||||
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService);
|
||||
|
||||
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference
|
||||
{
|
||||
@@ -60,13 +69,13 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly NestedContentValues _nestedContentValues;
|
||||
|
||||
public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService)
|
||||
public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService)
|
||||
: base(attribute)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_nestedContentValues = new NestedContentValues(contentTypeService);
|
||||
Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues));
|
||||
Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, textService));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -255,75 +264,25 @@ namespace Umbraco.Web.PropertyEditors
|
||||
}
|
||||
}
|
||||
|
||||
internal class NestedContentValidator : IValueValidator
|
||||
internal class NestedContentValidator : ComplexEditorValidator
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly NestedContentValues _nestedContentValues;
|
||||
|
||||
public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues)
|
||||
public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
: base(propertyEditors, dataTypeService, textService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_nestedContentValues = nestedContentValues;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
|
||||
protected override IEnumerable<ElementTypeValidationModel> GetElementsFromValue(object value)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _))
|
||||
foreach (var row in _nestedContentValues.GetPropertyValues(value, out _))
|
||||
{
|
||||
if (row.PropType == null) continue;
|
||||
|
||||
var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
|
||||
var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
if (propertyEditor == null) continue;
|
||||
|
||||
foreach (var validator in propertyEditor.GetValueEditor().Validators)
|
||||
{
|
||||
foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config))
|
||||
{
|
||||
result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage;
|
||||
validationResults.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Check mandatory
|
||||
if (row.PropType.Mandatory)
|
||||
{
|
||||
if (row.JsonRowValue[row.PropKey] == null)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
|
||||
? $"'{row.PropType.Name}' cannot be null"
|
||||
: row.PropType.MandatoryMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues))
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
|
||||
? $"'{row.PropType.Name}' cannot be empty"
|
||||
: row.PropType.MandatoryMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
}
|
||||
|
||||
// Check regex
|
||||
if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace()
|
||||
&& row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
var regex = new Regex(row.PropType.ValidationRegExp);
|
||||
if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString()))
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage)
|
||||
? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern"
|
||||
: row.PropType.ValidationRegExpMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
}
|
||||
var val = row.JsonRowValue[row.PropKey];
|
||||
yield return new ElementTypeValidationModel(val, row.PropType);
|
||||
}
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom <see cref="ValidationResult"/> that contains a list of nested validation results
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example, each <see cref="NestedValidationResults"/> represents validation results for a row in Nested Content
|
||||
/// </remarks>
|
||||
public class NestedValidationResults : ValidationResult
|
||||
{
|
||||
public NestedValidationResults(IEnumerable<ValidationResult> nested)
|
||||
: base(string.Empty)
|
||||
{
|
||||
ValidationResults = new List<ValidationResult>(nested);
|
||||
}
|
||||
|
||||
public IList<ValidationResult> ValidationResults { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom <see cref="ValidationResult"/> for content properties
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This clones the original result and then ensures the nested result if it's the correct type
|
||||
/// </remarks>
|
||||
public class PropertyValidationResult : ValidationResult
|
||||
{
|
||||
public PropertyValidationResult(ValidationResult nested)
|
||||
: base(nested.ErrorMessage, nested.MemberNames)
|
||||
{
|
||||
NestedResuls = nested as NestedValidationResults;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nested validation results for the content property
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There can be nested results for complex editors that contain other editors
|
||||
/// </remarks>
|
||||
public NestedValidationResults NestedResuls { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom json converter for <see cref="ValidationResult"/> and <see cref="PropertyValidationResult"/>
|
||||
/// </summary>
|
||||
internal class ValidationResultConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => typeof(ValidationResult).IsAssignableFrom(objectType);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var camelCaseSerializer = new JsonSerializer
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
foreach (var c in serializer.Converters)
|
||||
camelCaseSerializer.Converters.Add(c);
|
||||
|
||||
var validationResult = (ValidationResult)value;
|
||||
|
||||
if (validationResult is NestedValidationResults nestedResult)
|
||||
{
|
||||
if (nestedResult.ValidationResults.Count > 0)
|
||||
{
|
||||
var validationItems = JToken.FromObject(nestedResult.ValidationResults, camelCaseSerializer);
|
||||
validationItems.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var jo = new JObject();
|
||||
|
||||
if (!validationResult.ErrorMessage.IsNullOrWhiteSpace())
|
||||
{
|
||||
jo.Add("errorMessage", JToken.FromObject(validationResult.ErrorMessage, camelCaseSerializer));
|
||||
}
|
||||
|
||||
if (validationResult.MemberNames.Any())
|
||||
{
|
||||
jo.Add("memberNames", JToken.FromObject(validationResult.MemberNames, camelCaseSerializer));
|
||||
}
|
||||
|
||||
if (validationResult is PropertyValidationResult propertyValidationResult)
|
||||
{
|
||||
if (propertyValidationResult.NestedResuls?.ValidationResults.Count > 0)
|
||||
{
|
||||
jo.Add("nestedValidation", JToken.FromObject(propertyValidationResult.NestedResuls, camelCaseSerializer));
|
||||
}
|
||||
}
|
||||
|
||||
jo.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,8 +245,12 @@
|
||||
<Compile Include="PropertyEditors\BlockListConfigurationEditor.cs" />
|
||||
<Compile Include="PropertyEditors\BlockListPropertyEditor.cs" />
|
||||
<Compile Include="Compose\NestedContentPropertyComposer.cs" />
|
||||
<Compile Include="PropertyEditors\ComplexEditorValidator.cs" />
|
||||
<Compile Include="PropertyEditors\ParameterEditors\MultipleMediaPickerParameterEditor.cs" />
|
||||
<Compile Include="PropertyEditors\RichTextEditorPastedImages.cs" />
|
||||
<Compile Include="PropertyEditors\Validation\NestedValidationResults.cs" />
|
||||
<Compile Include="PropertyEditors\Validation\PropertyValidationResult.cs" />
|
||||
<Compile Include="PropertyEditors\Validation\ValidationResultConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\BlockEditorConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\BlockListPropertyValueConverter.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\PublishedSnapshotServiceOptions.cs" />
|
||||
|
||||
Reference in New Issue
Block a user