Fixes merge conflicts
This commit is contained in:
@@ -348,6 +348,10 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
"imageUrlGeneratorApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<ImageUrlGeneratorController>(
|
||||
controller => controller.GetCropUrl(null, null, null, null, null))
|
||||
},
|
||||
{
|
||||
"elementTypeApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<ElementTypeController>(
|
||||
controller => controller.GetAll())
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -405,6 +405,29 @@ namespace Umbraco.Web.Editors
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
return GetEmpty(contentType, parentId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty content item for the document type.
|
||||
/// </summary>
|
||||
/// <param name="contentTypeKey"></param>
|
||||
/// <param name="parentId"></param>
|
||||
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
|
||||
public ContentItemDisplay GetEmptyByKey(Guid contentTypeKey, int parentId)
|
||||
{
|
||||
var contentType = _contentTypeService.Get(contentTypeKey);
|
||||
if (contentType == null)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
return GetEmpty(contentType, parentId);
|
||||
}
|
||||
|
||||
private ContentItemDisplay GetEmpty(IContentType contentType, int parentId)
|
||||
{
|
||||
var emptyContent = _contentService.Create("", parentId, contentType.Alias, _webSecurity.GetUserId().ResultOr(0));
|
||||
var mapped = MapToDisplay(emptyContent);
|
||||
// translate the content type name if applicable
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Common.Attributes;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
[PluginController("UmbracoApi")]
|
||||
public class ElementTypeController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
|
||||
public ElementTypeController(IContentTypeService contentTypeService)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<object> GetAll()
|
||||
{
|
||||
return _contentTypeService
|
||||
.GetAllElementTypes()
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.Select(x => new
|
||||
{
|
||||
id = x.Id,
|
||||
key = x.Key,
|
||||
name = x.Name,
|
||||
description = x.Description,
|
||||
alias = x.Alias,
|
||||
icon = x.Icon
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
@@ -22,7 +23,28 @@ namespace Umbraco.Extensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the error to model state correctly for a property so we can use it on the client side.
|
||||
/// Adds the <see cref="ValidationResult"/> to the model state with the appropriate keys for property errors
|
||||
/// </summary>
|
||||
/// <param name="modelState"></param>
|
||||
/// <param name="result"></param>
|
||||
/// <param name="propertyAlias"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
internal static void AddPropertyValidationError(this ModelStateDictionary modelState,
|
||||
ValidationResult result, string propertyAlias, string culture = "", string segment = "")
|
||||
{
|
||||
modelState.AddValidationError(
|
||||
result,
|
||||
"_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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="ContentPropertyValidationResult"/> error to model state for a property so we can use it on the client side.
|
||||
/// </summary>
|
||||
/// <param name="modelState"></param>
|
||||
/// <param name="result"></param>
|
||||
@@ -31,15 +53,10 @@ namespace Umbraco.Extensions
|
||||
internal static void AddPropertyError(this ModelStateDictionary modelState,
|
||||
ValidationResult result, string propertyAlias, string culture = "", string segment = "")
|
||||
{
|
||||
if (culture == null)
|
||||
culture = "";
|
||||
modelState.AddValidationError(result, "_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);
|
||||
modelState.AddPropertyValidationError(new ContentPropertyValidationResult(result, culture, segment), propertyAlias, culture, segment);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs
|
||||
/// </summary>
|
||||
@@ -151,7 +168,7 @@ namespace Umbraco.Extensions
|
||||
}
|
||||
if (!withNames)
|
||||
{
|
||||
modelState.TryAddModelError($"{delimitedParts}", result.ErrorMessage);
|
||||
modelState.TryAddModelError($"{delimitedParts}", result.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
@@ -19,13 +21,16 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
/// </summary>
|
||||
internal abstract class ContentModelValidator
|
||||
{
|
||||
|
||||
protected IWebSecurity WebSecurity { get; }
|
||||
public IPropertyValidationService PropertyValidationService { get; }
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity)
|
||||
protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity, IPropertyValidationService propertyValidationService)
|
||||
{
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
WebSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
|
||||
PropertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +51,12 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
private readonly ILocalizedTextService _textService;
|
||||
|
||||
protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity)
|
||||
protected ContentModelValidator(
|
||||
ILogger logger,
|
||||
IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(logger, webSecurity, propertyValidationService)
|
||||
{
|
||||
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
|
||||
}
|
||||
@@ -126,18 +136,6 @@ namespace Umbraco.Web.BackOffice.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;
|
||||
@@ -157,7 +155,7 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
|
||||
var postedValue = postedProp.Value;
|
||||
|
||||
ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState, requiredDefaultMessages, formatDefaultMessages);
|
||||
ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState);
|
||||
|
||||
}
|
||||
|
||||
@@ -181,26 +179,29 @@ namespace Umbraco.Web.BackOffice.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(
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,14 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
/// </summary>
|
||||
internal class ContentSaveModelValidator : ContentModelValidator<IContent, ContentItemSave, ContentVariantSave>
|
||||
{
|
||||
public ContentSaveModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity, textService)
|
||||
public ContentSaveModelValidator(
|
||||
ILogger logger,
|
||||
IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(logger, webSecurity, textService, propertyValidationService)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,21 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IPropertyValidationService _propertyValidationService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILocalizedTextService _textService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IWebSecurity _webSecurity;
|
||||
|
||||
|
||||
public ContentSaveValidationFilter(ILogger logger, IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService, IContentService contentService, IUserService userService,
|
||||
IEntityService entityService)
|
||||
public ContentSaveValidationFilter(
|
||||
ILogger logger,
|
||||
IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService,
|
||||
IContentService contentService,
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
|
||||
@@ -47,12 +53,13 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
|
||||
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
|
||||
_entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
|
||||
_propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService));
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var model = (ContentItemSave) context.ActionArguments["contentItem"];
|
||||
var contentItemValidator = new ContentSaveModelValidator(_logger, _webSecurity, _textService);
|
||||
var contentItemValidator = new ContentSaveModelValidator(_logger, _webSecurity, _textService, _propertyValidationService);
|
||||
|
||||
if (!ValidateAtLeastOneVariantIsBeingSaved(model, context)) return;
|
||||
if (!contentItemValidator.ValidateExistingContent(model, context)) return;
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
private sealed class MediaItemSaveValidationFilter : IActionFilter
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IPropertyValidationService _propertyValidationService;
|
||||
|
||||
|
||||
private readonly ILogger _logger;
|
||||
@@ -30,20 +31,26 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
private readonly ILocalizedTextService _textService;
|
||||
private readonly IWebSecurity _webSecurity;
|
||||
|
||||
public MediaItemSaveValidationFilter(ILogger logger, IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService)
|
||||
public MediaItemSaveValidationFilter(
|
||||
ILogger logger,
|
||||
IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService,
|
||||
IMediaService mediaService,
|
||||
IEntityService entityService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
|
||||
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
|
||||
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
|
||||
_entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
|
||||
_propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService));
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var model = (MediaItemSave) context.ActionArguments["contentItem"];
|
||||
var contentItemValidator = new MediaSaveModelValidator(_logger, _webSecurity, _textService);
|
||||
var contentItemValidator = new MediaSaveModelValidator(_logger, _webSecurity, _textService, _propertyValidationService);
|
||||
|
||||
if (ValidateUserAccess(model, context))
|
||||
{
|
||||
|
||||
@@ -11,7 +11,12 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
/// </summary>
|
||||
internal class MediaSaveModelValidator : ContentModelValidator<IMedia, MediaItemSave, IContentProperties<ContentPropertyBasic>>
|
||||
{
|
||||
public MediaSaveModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity, textService)
|
||||
public MediaSaveModelValidator(
|
||||
ILogger logger,
|
||||
IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(logger, webSecurity, textService, propertyValidationService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
ILocalizedTextService textService,
|
||||
IMemberTypeService memberTypeService,
|
||||
IMemberService memberService,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(logger, webSecurity, textService)
|
||||
IShortStringHelper shortStringHelper,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(logger, webSecurity, textService, propertyValidationService)
|
||||
{
|
||||
_memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
|
||||
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
|
||||
|
||||
@@ -27,8 +27,16 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly IPropertyValidationService _propertyValidationService;
|
||||
|
||||
public MemberSaveValidationFilter(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper)
|
||||
public MemberSaveValidationFilter(
|
||||
ILogger logger,
|
||||
IWebSecurity webSecurity,
|
||||
ILocalizedTextService textService,
|
||||
IMemberTypeService memberTypeService,
|
||||
IMemberService memberService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
|
||||
@@ -36,12 +44,13 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
_memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
|
||||
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
|
||||
_shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
|
||||
_propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService));
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var model = (MemberSave)context.ActionArguments["contentItem"];
|
||||
var contentItemValidator = new MemberSaveModelValidator(_logger, _webSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper);
|
||||
var contentItemValidator = new MemberSaveModelValidator(_logger, _webSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper, _propertyValidationService);
|
||||
//now do each validation step
|
||||
if (contentItemValidator.ValidateExistingContent(model, context))
|
||||
if (contentItemValidator.ValidateProperties(model, model, context))
|
||||
|
||||
@@ -90,6 +90,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.AllowedTemplates = GetAllowedTemplates(source);
|
||||
target.ContentApps = _commonMapper.GetContentApps(source);
|
||||
target.ContentTypeId = source.ContentType.Id;
|
||||
target.ContentTypeKey = source.ContentType.Key;
|
||||
target.ContentTypeAlias = source.ContentType.Alias;
|
||||
target.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(_cultureDictionary, source.ContentType.Name);
|
||||
target.DocumentType = _commonMapper.GetContentType(source, context);
|
||||
|
||||
@@ -74,7 +74,15 @@ namespace Umbraco.Web.BackOffice.ModelBinders
|
||||
return;
|
||||
}
|
||||
|
||||
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
|
||||
var persistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
|
||||
BindModel(model, persistedContent, _modelBinderHelper, _umbracoMapper);
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
|
||||
internal static void BindModel(ContentItemSave model, IContent persistedContent, ContentModelBinderHelper modelBinderHelper, UmbracoMapper umbracoMapper)
|
||||
{
|
||||
model.PersistedContent =persistedContent;
|
||||
|
||||
//create the dto from the persisted model
|
||||
if (model.PersistedContent != null)
|
||||
@@ -82,7 +90,7 @@ namespace Umbraco.Web.BackOffice.ModelBinders
|
||||
foreach (var variant in model.Variants)
|
||||
{
|
||||
//map the property dto collection with the culture of the current variant
|
||||
variant.PropertyCollectionDto = _umbracoMapper.Map<ContentPropertyCollectionDto>(
|
||||
variant.PropertyCollectionDto = umbracoMapper.Map<ContentPropertyCollectionDto>(
|
||||
model.PersistedContent,
|
||||
context =>
|
||||
{
|
||||
@@ -92,13 +100,9 @@ namespace Umbraco.Web.BackOffice.ModelBinders
|
||||
});
|
||||
|
||||
//now map all of the saved values to the dto
|
||||
_modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto);
|
||||
modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto);
|
||||
}
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Newtonsoft.Json;
|
||||
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.
|
||||
///
|
||||
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
|
||||
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
|
||||
/// </remarks>
|
||||
public class ContentPropertyValidationResult : ValidationResult
|
||||
{
|
||||
private readonly string _culture;
|
||||
private readonly string _segment;
|
||||
|
||||
public ContentPropertyValidationResult(ValidationResult nested, string culture, string segment)
|
||||
: base(nested.ErrorMessage, nested.MemberNames)
|
||||
{
|
||||
ComplexEditorResults = nested as ComplexEditorValidationResult;
|
||||
_culture = culture;
|
||||
_segment = segment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nested validation results for the content property
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There can be nested results for complex editors that contain other editors
|
||||
/// </remarks>
|
||||
public ComplexEditorValidationResult ComplexEditorResults { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Return the <see cref="ValidationResult.ErrorMessage"/> if <see cref="ComplexEditorResults"/> is null, else the serialized
|
||||
/// complex validation results
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
if (ComplexEditorResults == null)
|
||||
return base.ToString();
|
||||
|
||||
var json = JsonConvert.SerializeObject(this, new ValidationResultConverter(_culture, _segment));
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom json converter for <see cref="ValidationResult"/> and <see cref="ContentPropertyValidationResult"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This converter is specifically used to convert validation results for content in order to be able to have nested
|
||||
/// validation results for complex editors.
|
||||
///
|
||||
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
|
||||
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
|
||||
/// </remarks>
|
||||
internal class ValidationResultConverter : JsonConverter
|
||||
{
|
||||
private readonly string _culture;
|
||||
private readonly string _segment;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture of the containing property which will be transfered to all child model state</param>
|
||||
/// <param name="segment">The segment of the containing property which will be transfered to all child model state</param>
|
||||
public ValidationResultConverter(string culture = "", string segment = "")
|
||||
{
|
||||
_culture = culture;
|
||||
_segment = segment;
|
||||
}
|
||||
|
||||
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 ComplexEditorValidationResult nestedResult && nestedResult.ValidationResults.Count > 0)
|
||||
{
|
||||
var ja = new JArray();
|
||||
foreach(var nested in nestedResult.ValidationResults)
|
||||
{
|
||||
// recurse to write out the ComplexEditorElementTypeValidationResult
|
||||
var block = JObject.FromObject(nested, camelCaseSerializer);
|
||||
ja.Add(block);
|
||||
}
|
||||
if (nestedResult.ValidationResults.Count > 0)
|
||||
{
|
||||
ja.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
else if (validationResult is ComplexEditorElementTypeValidationResult elementTypeValidationResult && elementTypeValidationResult.ValidationResults.Count > 0)
|
||||
{
|
||||
var joElementType = new JObject
|
||||
{
|
||||
{ "$id", elementTypeValidationResult.BlockId },
|
||||
|
||||
// We don't use this anywhere, though it's nice for debugging
|
||||
{ "$elementTypeAlias", elementTypeValidationResult.ElementTypeAlias }
|
||||
};
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
|
||||
// loop over property validations
|
||||
foreach (var propTypeResult in elementTypeValidationResult.ValidationResults)
|
||||
{
|
||||
// group the results by their type and iterate the groups
|
||||
foreach (var result in propTypeResult.ValidationResults.GroupBy(x => x.GetType()))
|
||||
{
|
||||
// if the group's type isn't ComplexEditorValidationResult then it will in 99% of cases be
|
||||
// just ValidationResult for whcih we want to create the sub "ModelState" data. If it's not a normal
|
||||
// ValidationResult it will still just be converted to normal ModelState.
|
||||
|
||||
if (result.Key == typeof(ComplexEditorValidationResult))
|
||||
{
|
||||
// if it's ComplexEditorValidationResult then there can only be one which is validated so just get the single
|
||||
if (result.Any())
|
||||
{
|
||||
var complexResult = result.Single();
|
||||
// recurse to get the validation result object
|
||||
var obj = JToken.FromObject(complexResult, camelCaseSerializer);
|
||||
joElementType.Add(propTypeResult.PropertyTypeAlias, obj);
|
||||
|
||||
// For any nested property error we add the model state as empty state for that nested property
|
||||
// NOTE: Instead of the empty validation message we could put in the translated
|
||||
// "errors/propertyHasErrors" message, however I think that leaves for less flexibility since it could/should be
|
||||
// up to the front-end validator to show whatever message it wants (if any) for an error indicating a nested property error.
|
||||
// Will leave blank.
|
||||
modelState.AddPropertyValidationError(new ValidationResult(string.Empty), propTypeResult.PropertyTypeAlias, _culture, _segment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var v in result)
|
||||
{
|
||||
modelState.AddPropertyValidationError(v, propTypeResult.PropertyTypeAlias, _culture, _segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modelState.Count > 0)
|
||||
{
|
||||
joElementType.Add("ModelState", JObject.FromObject(modelState.ToErrorDictionary()));
|
||||
}
|
||||
|
||||
joElementType.WriteTo(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (validationResult is ContentPropertyValidationResult propertyValidationResult
|
||||
&& propertyValidationResult.ComplexEditorResults?.ValidationResults.Count > 0)
|
||||
{
|
||||
// recurse to write out the NestedValidationResults
|
||||
var obj = JToken.FromObject(propertyValidationResult.ComplexEditorResults, camelCaseSerializer);
|
||||
obj.WriteTo(writer);
|
||||
}
|
||||
|
||||
var jo = new JObject();
|
||||
if (!validationResult.ErrorMessage.IsNullOrWhiteSpace())
|
||||
{
|
||||
var errObj = JToken.FromObject(validationResult.ErrorMessage, camelCaseSerializer);
|
||||
jo.Add("errorMessage", errObj);
|
||||
}
|
||||
if (validationResult.MemberNames.Any())
|
||||
{
|
||||
var memberNamesObj = JToken.FromObject(validationResult.MemberNames, camelCaseSerializer);
|
||||
jo.Add("memberNames", memberNamesObj);
|
||||
}
|
||||
if (jo.HasValues)
|
||||
jo.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user