Skip to content

Commit

Permalink
Validation for block level variation (#17355)
Browse files Browse the repository at this point in the history
* Validation for block level variation

* Make the "missing property value" JSON path expression valid

* Update src/Umbraco.Core/Services/ContentValidationServiceBase.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Core/Services/ContentValidationServiceBase.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
  • Loading branch information
kjac and Zeegaan authored Oct 31, 2024
1 parent 3ecd5b4 commit 5fe91f8
Show file tree
Hide file tree
Showing 34 changed files with 1,246 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;

Expand Down Expand Up @@ -100,7 +101,7 @@ protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, T

var errors = new SortedDictionary<string, string[]>();

var missingPropertyModels = new List<PropertyValidationResponseModel>();
var validationErrorExpressionRoot = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}";
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
{
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
Expand All @@ -109,30 +110,23 @@ protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, T
&& value.Segment == validationError.Segment);
if (requestValue is null)
{
missingPropertyModels.Add(MapMissingProperty(validationError));
errors.Add(
$"{validationErrorExpressionRoot}[{JsonPathExpression.MissingPropertyValue(validationError.Alias, validationError.Culture, validationError.Segment)}].{nameof(ValueModelBase.Value)}",
validationError.ErrorMessages);
continue;
}

var index = requestModel.Values.IndexOf(requestValue);
var key = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}";
errors.Add(key, validationError.ErrorMessages);
errors.Add(
$"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}",
validationError.ErrorMessages);
}

return OperationStatusResult(status, problemDetailsBuilder
=> BadRequest(problemDetailsBuilder
.WithTitle("Validation failed")
.WithDetail("One or more properties did not pass validation")
.WithRequestModelErrors(errors)
.WithExtension("missingValues", missingPropertyModels.ToArray())
.Build()));
}

private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) =>
new()
{
Alias = source.Alias,
Segment = source.Segment,
Culture = source.Culture,
Messages = source.ErrorMessages,
};
}
4 changes: 3 additions & 1 deletion src/Umbraco.Core/Models/IDataValueEditor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;

namespace Umbraco.Cms.Core.Models;
Expand Down Expand Up @@ -42,7 +43,8 @@ public interface IDataValueEditor
/// <param name="value">The property value.</param>
/// <param name="required">A value indicating whether the property value is required.</param>
/// <param name="format">A specific format (regex) that the property value must respect.</param>
IEnumerable<ValidationResult> Validate(object? value, bool required, string? format);
/// <param name="validationContext">The context in which the property value is being validated.</param>
IEnumerable<ValidationResult> Validate(object? value, bool required, string? format, PropertyValidationContext validationContext);

/// <summary>
/// Converts a value posted by the editor to a property value.
Expand Down
22 changes: 22 additions & 0 deletions src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Umbraco.Cms.Core.Models.Validation;

public sealed class PropertyValidationContext
{
public required string? Culture { get; init; }

public required string? Segment { get; init; }

public required IEnumerable<string> CulturesBeingValidated { get; init; }

public required IEnumerable<string> SegmentsBeingValidated { get; init; }

public static PropertyValidationContext Empty() => new()
{
Culture = null, Segment = null, CulturesBeingValidated = [], SegmentsBeingValidated = []
};

public static PropertyValidationContext CultureAndSegment(string? culture, string? segment) => new()
{
Culture = culture, Segment = segment, CulturesBeingValidated = [], SegmentsBeingValidated = []
};
}
3 changes: 2 additions & 1 deletion src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;

Expand Down Expand Up @@ -85,7 +86,7 @@ public virtual IEnumerable<ValidationResult> Validate(IDictionary<string, object
=> Fields
.SelectMany(field =>
configuration.TryGetValue(field.Key, out var value)
? field.Validators.SelectMany(validator => validator.Validate(value, null, null))
? field.Validators.SelectMany(validator => validator.Validate(value, null, null, PropertyValidationContext.Empty()))
: Enumerable.Empty<ValidationResult>())
.ToArray();

Expand Down
5 changes: 3 additions & 2 deletions src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
Expand Down Expand Up @@ -105,10 +106,10 @@ public DataValueEditor(
public List<IValueValidator> Validators { get; private set; } = new();

/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object? value, bool required, string? format)
public IEnumerable<ValidationResult> Validate(object? value, bool required, string? format, PropertyValidationContext validationContext)
{
List<ValidationResult>? results = null;
var r = Validators.SelectMany(v => v.Validate(value, ValueType, ConfigurationObject)).ToList();
var r = Validators.SelectMany(v => v.Validate(value, ValueType, ConfigurationObject, validationContext)).ToList();
if (r.Any())
{
results = r;
Expand Down
4 changes: 3 additions & 1 deletion src/Umbraco.Core/PropertyEditors/IValueValidator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;

namespace Umbraco.Cms.Core.PropertyEditors;

Expand All @@ -13,12 +14,13 @@ public interface IValueValidator
/// <param name="value">The value to validate.</param>
/// <param name="valueType">The value type.</param>
/// <param name="dataTypeConfiguration">A datatype configuration.</param>
/// <param name="validationContext">The context in which the value is being validated.</param>
/// <returns>Validation results.</returns>
/// <remarks>
/// <para>
/// The value can be a string, a Json structure (JObject, JArray...)... corresponding to what was posted by an
/// editor.
/// </para>
/// </remarks>
IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration);
IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.PropertyEditors.Validation;

public static class JsonPathExpression
{
public static string MissingPropertyValue(string propertyAlias, string? culture, string? segment)
=> $"?(@.alias == '{propertyAlias}' && @.culture == {(culture.IsNullOrWhiteSpace() ? "null" : $"'{culture}'")} && @.segment == {(segment.IsNullOrWhiteSpace() ? "null" : $"'{segment}'")})";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.PropertyEditors.Validators;
Expand All @@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
/// </summary>
public class DateTimeValidator : IValueValidator
{
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
// don't validate if empty
if (value == null || value.ToString().IsNullOrWhiteSpace())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.PropertyEditors.Validators;
Expand All @@ -9,7 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
public sealed class DecimalValidator : IValueValidator
{
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value == null || value.ToString() == string.Empty)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;

namespace Umbraco.Cms.Core.PropertyEditors.Validators;

Expand All @@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
public sealed class EmailValidator : IValueValidator
{
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
var asString = value == null ? string.Empty : value.ToString();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.PropertyEditors.Validators;
Expand All @@ -9,7 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
public sealed class IntegerValidator : IValueValidator
{
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value != null && value.ToString() != string.Empty)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Core.PropertyEditors.Validators;
Expand Down Expand Up @@ -49,7 +50,7 @@ public RegexValidator(string regex)
=> _regex = regex;

/// <inheritdoc cref="IValueValidator.Validate" />
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (_regex == null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;

Expand All @@ -20,7 +21,7 @@ public RequiredValidator()
}

/// <inheritdoc cref="IValueValidator.Validate" />
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration) =>
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) =>
ValidateRequired(value, valueType);

/// <inheritdoc cref="IValueRequiredValidator.ValidateRequired" />
Expand Down
15 changes: 6 additions & 9 deletions src/Umbraco.Core/Services/ContentPublishingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,13 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
}

ContentValidationResult validationResult = await ValidateCurrentContentAsync(content, cultures);

var errors = validationResult.ValidationErrors.Where(err =>
cultures.Contains(err.Culture ?? "*", StringComparer.InvariantCultureIgnoreCase));
if (errors.Any())
if (validationResult.ValidationErrors.Any())
{
scope.Complete();
return Attempt.FailWithStatus(ContentPublishingOperationStatus.ContentInvalid, new ContentPublishingResult
{
Content = content,
InvalidPropertyAliases = errors.Select(property => property.Alias).ToArray()
?? Enumerable.Empty<string>()
InvalidPropertyAliases = validationResult.ValidationErrors.Select(property => property.Alias).ToArray()
});
}

Expand Down Expand Up @@ -131,11 +127,12 @@ private async Task<ContentValidationResult> ValidateCurrentContentAsync(IContent
var model = new ContentUpdateModel()
{
InvariantName = content.Name,
InvariantProperties = cultures.Contains("*") ? content.Properties.Where(x=>x.PropertyType.VariesByCulture() is false).Select(x=> new PropertyValueModel()
// NOTE KJA: this needs redoing; we need to make an informed decision whether to include invariant properties, depending on if editing invariant properties is allowed on all variants, or if the default language is included in cultures
InvariantProperties = content.Properties.Where(x => x.PropertyType.VariesByCulture() is false).Select(x => new PropertyValueModel()
{
Alias = x.Alias,
Value = x.GetValue()
}) : Array.Empty<PropertyValueModel>(),
}),
Variants = cultures.Select(culture => new VariantModel()
{
Name = content.GetPublishName(culture) ?? string.Empty,
Expand All @@ -149,7 +146,7 @@ private async Task<ContentValidationResult> ValidateCurrentContentAsync(IContent
})
};
IContentType? contentType = _contentTypeService.Get(content.ContentType.Key)!;
ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType);
ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType, cultures);
return validationResult;
}

Expand Down
Loading

0 comments on commit 5fe91f8

Please sign in to comment.