Server side validation for property editors (multiple text strings) (#18581)
* Server-side validation for multiple text strings property editor. * Added unit tests for block list min/max server validation. * Add danish translations * Add test showing issue * Fix issue --------- Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@@ -16,12 +16,22 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockEditorMinMaxValidatorBase{TValue, TLayout}"/> class.
|
||||
/// </summary>
|
||||
protected BlockEditorMinMaxValidatorBase(ILocalizedTextService textService) => TextService = textService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILocalizedTextService"/>
|
||||
/// </summary>
|
||||
protected ILocalizedTextService TextService { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the number of blocks are within the configured minimum and maximum values.
|
||||
/// </summary>
|
||||
protected IEnumerable<ValidationResult> ValidateNumberOfBlocks(BlockEditorData<TValue, TLayout>? blockEditorData, int? min, int? max)
|
||||
{
|
||||
var numberOfBlocks = blockEditorData?.Layout?.Count() ?? 0;
|
||||
@@ -35,8 +45,8 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
|
||||
TextService.Localize(
|
||||
"validation",
|
||||
"entriesShort",
|
||||
new[] { min.ToString(), (min - numberOfBlocks).ToString(), }),
|
||||
new[] { "minCount" });
|
||||
[min.ToString(), (min - numberOfBlocks).ToString(),]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +56,8 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
|
||||
TextService.Localize(
|
||||
"validation",
|
||||
"entriesExceed",
|
||||
new[] { max.ToString(), (numberOfBlocks - max).ToString(), }),
|
||||
new[] { "maxCount" });
|
||||
[max.ToString(), (numberOfBlocks - max).ToString(),]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a block list property editor.
|
||||
/// Represents a block list property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.BlockList,
|
||||
@@ -19,6 +19,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
{
|
||||
private readonly IIOHelper _ioHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public BlockListPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
@@ -27,6 +30,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Use constructor that doesn't take PropertyEditorCollection, scheduled for removal in V15")]
|
||||
public BlockListPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
@@ -38,6 +44,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -50,6 +57,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? MergeVariantInvariantPropertyValue(
|
||||
object? sourceValue,
|
||||
object? targetValue,
|
||||
@@ -60,10 +68,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
return valueEditor.MergeVariantInvariantPropertyValue(sourceValue, targetValue, canUpdateInvariantData, allowedCultures);
|
||||
}
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
new BlockListConfigurationEditor(_ioHelper);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -24,12 +24,18 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 15.")]
|
||||
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: this(dataValueEditorFactory,blockValuePropertyIndexValueFactory, StaticServiceProvider.Instance.GetRequiredService<IJsonSerializer>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
|
||||
/// </summary>
|
||||
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
@@ -38,21 +44,27 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
SupportsReadOnly = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
|
||||
|
||||
#region Value Editor
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="BlockEditorDataConverter"/> for use with the block list editor property value editor.
|
||||
/// Instantiates a new <see cref="BlockEditorDataConverter{BlockListValue, BlockListLayoutItem}"/> for use with the block list editor property value editor.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of <see cref="BlockListEditorDataConverter"/>.</returns>
|
||||
protected virtual BlockEditorDataConverter<BlockListValue, BlockListLayoutItem> CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(_jsonSerializer);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IDataValueEditor CreateValueEditor() =>
|
||||
DataValueEditorFactory.Create<BlockListEditorPropertyValueEditor>(Attribute!, CreateBlockEditorDataConverter());
|
||||
|
||||
/// <summary>
|
||||
/// Defines the value editor for the block list property editors.
|
||||
/// </summary>
|
||||
internal class BlockListEditorPropertyValueEditor : BlockEditorPropertyValueEditor<BlockListValue, BlockListLayoutItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListEditorPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public BlockListEditorPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
BlockEditorDataConverter<BlockListValue, BlockListLayoutItem> blockEditorDataConverter,
|
||||
@@ -78,8 +90,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
/// <inheritdoc />
|
||||
public override IValueRequiredValidator RequiredValidator => new BlockListValueRequiredValidator(JsonSerializer);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override BlockListValue CreateWithLayout(IEnumerable<BlockListLayoutItem> layout) => new(layout);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for block list property editors.
|
||||
/// </summary>
|
||||
private class MinMaxValidator : BlockEditorMinMaxValidatorBase<BlockListValue, BlockListLayoutItem>
|
||||
{
|
||||
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockEditorValues;
|
||||
@@ -104,12 +120,11 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
|
||||
{
|
||||
var configuration = ConfigurationObject as BlockListConfiguration;
|
||||
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -6,14 +6,17 @@ using Umbraco.Cms.Core.Exceptions;
|
||||
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;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a multiple text string property editor.
|
||||
/// Represents a multiple text string property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.MultipleTextstring,
|
||||
@@ -24,7 +27,7 @@ public class MultipleTextStringPropertyEditor : DataEditor
|
||||
private readonly IIOHelper _ioHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyEditor" /> class.
|
||||
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyEditor" /> class.
|
||||
/// </summary>
|
||||
public MultipleTextStringPropertyEditor(IIOHelper ioHelper, IDataValueEditorFactory dataValueEditorFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
@@ -42,37 +45,42 @@ public class MultipleTextStringPropertyEditor : DataEditor
|
||||
new MultipleTextStringConfigurationEditor(_ioHelper);
|
||||
|
||||
/// <summary>
|
||||
/// Custom value editor so we can format the value for the editor and the database
|
||||
/// Defines the value editor for the multiple text string property editor.
|
||||
/// </summary>
|
||||
internal class MultipleTextStringPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
private static readonly string NewLine = "\n";
|
||||
private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" };
|
||||
private static readonly string _newLine = "\n";
|
||||
private static readonly string[] _newLineDelimiters = { "\r\n", "\r", "\n" };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public MultipleTextStringPropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
DataEditorAttribute attribute)
|
||||
DataEditorAttribute attribute,
|
||||
ILocalizedTextService localizedTextService)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
Validators.AddRange(new MinMaxValidator(localizedTextService));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom FormatValidator is used as for multiple text strings, each string should individually be checked
|
||||
/// against the configured regular expression, rather than the JSON representing all the strings as a whole.
|
||||
/// A custom <see href="IValueFormatValidator" /> is used as for multiple text strings, each string should individually
|
||||
/// be checked against the configured regular expression, rather than the JSON representing all the strings as a whole.
|
||||
/// </summary>
|
||||
public override IValueFormatValidator FormatValidator => new MultipleTextStringFormatValidator();
|
||||
|
||||
/// <summary>
|
||||
/// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the
|
||||
/// string
|
||||
/// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the
|
||||
/// string.
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end
|
||||
/// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end.
|
||||
/// </remarks>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
{
|
||||
@@ -93,30 +101,35 @@ public class MultipleTextStringPropertyEditor : DataEditor
|
||||
// only allow the max if over 0
|
||||
if (max > 0)
|
||||
{
|
||||
return string.Join(NewLine, value.Take(max));
|
||||
return string.Join(_newLine, value.Take(max));
|
||||
}
|
||||
|
||||
return string.Join(NewLine, value);
|
||||
return string.Join(_newLine, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
{
|
||||
var value = property.GetValue(culture, segment);
|
||||
|
||||
// The legacy property editor saved this data as new line delimited! strange but we have to maintain that.
|
||||
return value is string stringValue
|
||||
? stringValue.Split(NewLineDelimiters, StringSplitOptions.None)
|
||||
? stringValue.Split(_newLineDelimiters, StringSplitOptions.None)
|
||||
: Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom <see href="IValueFormatValidator" /> to check each string against the configured format.
|
||||
/// </summary>
|
||||
internal class MultipleTextStringFormatValidator : IValueFormatValidator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> ValidateFormat(object? value, string valueType, string format)
|
||||
{
|
||||
if (value is not IEnumerable<string> textStrings)
|
||||
{
|
||||
return Enumerable.Empty<ValidationResult>();
|
||||
return [];
|
||||
}
|
||||
|
||||
var textStringValidator = new RegexValidator();
|
||||
@@ -129,7 +142,60 @@ public class MultipleTextStringPropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
return Enumerable.Empty<ValidationResult>();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for the multiple text strings property editor.
|
||||
/// </summary>
|
||||
internal class MinMaxValidator : IValueValidator
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (dataTypeConfiguration is not MultipleTextStringConfiguration multipleTextStringConfiguration)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// If we have a null value, treat as an empty collection for minimum number validation.
|
||||
if (value is not IEnumerable<string> stringValues)
|
||||
{
|
||||
stringValues = [];
|
||||
}
|
||||
|
||||
var stringCount = stringValues.Count();
|
||||
|
||||
if (stringCount < multipleTextStringConfiguration.Min)
|
||||
{
|
||||
if (stringCount == 1)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "outOfRangeSingleItemMinimum", [multipleTextStringConfiguration.Min.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "outOfRangeMultipleItemsMinimum", [stringCount.ToString(), multipleTextStringConfiguration.Min.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
|
||||
if (multipleTextStringConfiguration.Max > 0 && stringCount > multipleTextStringConfiguration.Max)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
_localizedTextService.Localize("validation", "outOfRangeMultipleItemsMaximum", [stringCount.ToString(), multipleTextStringConfiguration.Max.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user