diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index fe419a6566..f1da661848 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -10,8 +10,11 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; -internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor +internal sealed partial class ColorPickerConfigurationEditor : ConfigurationEditor { + /// + /// Initializes a new instance of the class. + /// public ColorPickerConfigurationEditor(IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) : base(ioHelper) { @@ -19,13 +22,17 @@ internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor + /// Initializes a new instance of the class. + /// public ColorListValidator(IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) => _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + /// public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { var stringValue = value?.ToString(); @@ -46,17 +53,53 @@ internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor(StringComparer.OrdinalIgnoreCase); + var duplicates = new List(); foreach (ColorPickerConfiguration.ColorPickerItem item in items) { - if (Regex.IsMatch(item.Value, "^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase) == false) + if (ColorPattern().IsMatch(item.Value) == false) { - yield return new ValidationResult($"The value {item.Value} is not a valid hex color", new[] { "items" }); + yield return new ValidationResult($"The value {item.Value} is not a valid hex color", ["items"]); + continue; + } + + var normalized = Normalize(item.Value); + if (seen.Add(normalized) is false) + { + duplicates.Add(normalized); } } + + if (duplicates.Count > 0) + { + yield return new ValidationResult( + $"Duplicate color values are not allowed: {string.Join(", ", duplicates)}", + ["items"]); + } } + + private static string Normalize(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var normalizedValue = value.Trim().ToLowerInvariant(); + + if (normalizedValue.Length == 3) + { + normalizedValue = $"{normalizedValue[0]}{normalizedValue[0]}{normalizedValue[1]}{normalizedValue[1]}{normalizedValue[2]}{normalizedValue[2]}"; + } + + return normalizedValue; + } + + [GeneratedRegex("^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex ColorPattern(); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs index 72de48631f..01b8a428c3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs @@ -58,4 +58,24 @@ public class ColorListValidatorTest PropertyValidationContext.Empty()); Assert.AreEqual(2, result.Count()); } + + [Test] + public void Validates_Color_Vals_Are_Unique() + { + var validator = new ColorPickerConfigurationEditor.ColorListValidator(ConfigurationEditorJsonSerializer()); + var result = + validator.Validate( + new JsonArray( + JsonNode.Parse("""{"value": "FFFFFF", "label": "One"}"""), + JsonNode.Parse("""{"value": "000000", "label": "Two"}"""), + JsonNode.Parse("""{"value": "FF00AA", "label": "Three"}"""), + JsonNode.Parse("""{"value": "fff", "label": "Four"}"""), + JsonNode.Parse("""{"value": "000000", "label": "Five"}"""), + JsonNode.Parse("""{"value": "F0A", "label": "Six"}""")), + null, + null, + PropertyValidationContext.Empty()); + Assert.AreEqual(1, result.Count()); + Assert.IsTrue(result.First().ErrorMessage.Contains("ffffff, 000000, ff00aa")); + } }