Files
Umbraco-CMS/src/Umbraco.Core/Services/ContentValidationServiceBase.cs
Kenn Jacobsen aaf7075313 Property level validation for Management API (#15644)
* Property level validation for content - initial implementation

* Always succeed create/update regardless of property level validation errors

* Move old complex editor validation classes to Web.BackOffice so they will be deleted

* Include operation status and property validation errors in ProblemDetails

* Refactor property validation to its own service(s)

* Make the problem details builder a little more generic towards extensions

* Validation for item and branch publish

* Moved malplaced test

* Get rid of a TODO

* Integration tests for content validation service

* Simplify validation service

* Add missing response types to create and update for document and media

* Remove test that no longer applies

* Use "errors" for model validation errors (property validation errors)

* Split create/update and validation into their own endpoints

* Fix forward merge

* Correct wrong assumption for missing properties

* Remove localization from validation error messages - decreases dependencies, adds a lot of obsolete constructors

* Reuse existing validation service + support custom error messages

* Fix merge errors

* Review comments
2024-01-31 10:40:58 +01:00

127 lines
5.1 KiB
C#

using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
internal abstract class ContentValidationServiceBase<TContentType>
where TContentType : IContentTypeComposition
{
private readonly ILanguageService _languageService;
private readonly IPropertyValidationService _propertyValidationService;
protected ContentValidationServiceBase(
IPropertyValidationService propertyValidationService,
ILanguageService languageService)
{
_propertyValidationService = propertyValidationService;
_languageService = languageService;
}
protected async Task<ContentValidationResult> HandlePropertiesValidationAsync(
ContentEditingModelBase contentEditingModelBase,
TContentType contentType)
{
var validationErrors = new List<PropertyValidationError>();
IPropertyType[] contentTypePropertyTypes = contentType.CompositionPropertyTypes.ToArray();
IPropertyType[] invariantPropertyTypes = contentTypePropertyTypes
.Where(propertyType => propertyType.VariesByNothing())
.ToArray();
IPropertyType[] variantPropertyTypes = contentTypePropertyTypes.Except(invariantPropertyTypes).ToArray();
foreach (IPropertyType propertyType in invariantPropertyTypes)
{
validationErrors.AddRange(ValidateProperty(contentEditingModelBase, propertyType, null, null));
}
if (variantPropertyTypes.Any() is false)
{
return new ContentValidationResult { ValidationErrors = validationErrors };
}
var cultures = (await _languageService.GetAllAsync()).Select(language => language.IsoCode).ToArray();
// we don't have any managed segments, so we have to make do with the ones passed in the model
var segments = contentEditingModelBase.Variants.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment).ToArray();
foreach (IPropertyType propertyType in variantPropertyTypes)
{
foreach (var culture in cultures)
{
foreach (var segment in segments)
{
validationErrors.AddRange(ValidateProperty(contentEditingModelBase, propertyType, culture, segment));
}
}
}
return new ContentValidationResult { ValidationErrors = validationErrors };
}
private IEnumerable<PropertyValidationError> ValidateProperty(ContentEditingModelBase contentEditingModelBase, IPropertyType propertyType, string? culture, string? segment)
{
IEnumerable<PropertyValueModel>? properties = culture is null && segment is null
? contentEditingModelBase.InvariantProperties
: contentEditingModelBase
.Variants
.FirstOrDefault(variant => variant.Culture == culture && variant.Segment == segment)?
.Properties;
PropertyValueModel? propertyValueModel = properties?.FirstOrDefault(p => p.Alias == propertyType.Alias);
ValidationResult[] validationResults = _propertyValidationService
.ValidatePropertyValue(propertyType, propertyValueModel?.Value)
.ToArray();
if (validationResults.Any() is false)
{
return Enumerable.Empty<PropertyValidationError>();
}
PropertyValidationError[] validationErrors = validationResults
.SelectMany(validationResult => ExtractPropertyValidationResultJsonPath(validationResult, propertyType.Alias, culture, segment))
.ToArray();
if (validationErrors.Any() is false)
{
validationErrors = new[]
{
new PropertyValidationError
{
JsonPath = string.Empty,
ErrorMessages = validationResults.Select(v => v.ErrorMessage).WhereNotNull().ToArray(),
Alias = propertyType.Alias,
Culture = culture,
Segment = segment
}
};
}
return validationErrors;
}
private IEnumerable<PropertyValidationError> ExtractPropertyValidationResultJsonPath(ValidationResult validationResult, string alias, string? culture, string? segment)
{
if (validationResult is not INestedValidationResults nestedValidationResults)
{
return Enumerable.Empty<PropertyValidationError>();
}
JsonPathValidationError[] results = nestedValidationResults
.ValidationResults
.SelectMany(JsonPathValidator.ExtractJsonPathValidationErrors)
.ToArray();
return results.Select(item => new PropertyValidationError
{
JsonPath = item.JsonPath,
ErrorMessages = item.ErrorMessages.ToArray(),
Alias = alias,
Culture = culture,
Segment = segment
}).ToArray();
}
}