2022-06-07 15:28:38 +02:00
using System.ComponentModel.DataAnnotations ;
2024-03-04 12:50:24 +01:00
using Microsoft.Extensions.DependencyInjection ;
2021-08-23 14:28:44 +02:00
using Umbraco.Cms.Core.Cache ;
2024-03-04 12:50:24 +01:00
using Umbraco.Cms.Core.DependencyInjection ;
using Umbraco.Cms.Core.Dictionary ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Models ;
2024-10-31 10:04:54 +01:00
using Umbraco.Cms.Core.Models.Validation ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.PropertyEditors ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
namespace Umbraco.Cms.Core.Services ;
public class PropertyValidationService : IPropertyValidationService
2019-02-21 14:46:14 +11:00
{
2022-06-07 15:28:38 +02:00
private readonly IDataTypeService _dataTypeService ;
private readonly ILocalizedTextService _textService ;
private readonly PropertyEditorCollection _propertyEditors ;
private readonly IValueEditorCache _valueEditorCache ;
2024-03-04 12:50:24 +01:00
private readonly ICultureDictionary _cultureDictionary ;
2022-06-07 15:28:38 +02:00
2024-03-04 12:50:24 +01:00
[Obsolete("Use the constructor that accepts ICultureDictionary. Will be removed in V15.")]
2022-06-07 15:28:38 +02:00
public PropertyValidationService (
PropertyEditorCollection propertyEditors ,
IDataTypeService dataTypeService ,
ILocalizedTextService textService ,
IValueEditorCache valueEditorCache )
2024-03-04 12:50:24 +01:00
: this ( propertyEditors , dataTypeService , textService , valueEditorCache , StaticServiceProvider . Instance . GetRequiredService < ICultureDictionary > ( ) )
2024-01-31 10:40:58 +01:00
{
}
public PropertyValidationService (
PropertyEditorCollection propertyEditors ,
IDataTypeService dataTypeService ,
2024-03-04 12:50:24 +01:00
ILocalizedTextService textService ,
IValueEditorCache valueEditorCache ,
ICultureDictionary cultureDictionary )
2022-06-07 15:28:38 +02:00
{
_propertyEditors = propertyEditors ;
_dataTypeService = dataTypeService ;
_textService = textService ;
_valueEditorCache = valueEditorCache ;
2024-03-04 12:50:24 +01:00
_cultureDictionary = cultureDictionary ;
2022-06-07 15:28:38 +02:00
}
/// <inheritdoc />
public IEnumerable < ValidationResult > ValidatePropertyValue (
IPropertyType propertyType ,
2024-10-31 10:04:54 +01:00
object? postedValue ,
PropertyValidationContext validationContext )
2019-02-21 14:46:14 +11:00
{
2022-06-07 15:28:38 +02:00
if ( propertyType is null )
2019-02-21 14:46:14 +11:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( propertyType ) ) ;
2019-02-21 14:46:14 +11:00
}
2024-10-31 10:04:54 +01:00
IDataType ? dataType = GetDataType ( propertyType ) ;
2022-06-07 15:28:38 +02:00
if ( dataType = = null )
2020-06-15 23:05:32 +10:00
{
2022-06-07 15:28:38 +02:00
throw new InvalidOperationException ( "No data type found by id " + propertyType . DataTypeId ) ;
}
2020-06-15 23:05:32 +10:00
2024-10-31 10:04:54 +01:00
IDataEditor ? dataEditor = GetDataEditor ( propertyType ) ;
if ( dataEditor = = null )
2022-06-07 15:28:38 +02:00
{
throw new InvalidOperationException ( "No property editor found by alias " +
propertyType . PropertyEditorAlias ) ;
2020-06-15 23:05:32 +10:00
}
2024-10-31 10:04:54 +01:00
return ValidatePropertyValue ( dataEditor , dataType , postedValue , propertyType . Mandatory , propertyType . ValidationRegExp , propertyType . MandatoryMessage , propertyType . ValidationRegExpMessage , validationContext ) ;
2022-06-07 15:28:38 +02:00
}
2024-10-31 10:04:54 +01:00
[Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
public IEnumerable < ValidationResult > ValidatePropertyValue (
IPropertyType propertyType ,
object? postedValue )
= > ValidatePropertyValue ( propertyType , postedValue , PropertyValidationContext . Empty ( ) ) ;
2022-06-07 15:28:38 +02:00
/// <inheritdoc />
public IEnumerable < ValidationResult > ValidatePropertyValue (
IDataEditor editor ,
IDataType dataType ,
object? postedValue ,
bool isRequired ,
string? validationRegExp ,
string? isRequiredMessage ,
2024-10-31 10:04:54 +01:00
string? validationRegExpMessage ,
PropertyValidationContext validationContext )
2022-06-07 15:28:38 +02:00
{
// 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.
2024-01-31 10:40:58 +01:00
var requiredDefaultMessages = new [ ] { Constants . Validation . ErrorMessages . Properties . Missing } ;
var formatDefaultMessages = new [ ] { Constants . Validation . ErrorMessages . Properties . PatternMismatch } ;
2022-06-07 15:28:38 +02:00
IDataValueEditor valueEditor = _valueEditorCache . GetValueEditor ( editor , dataType ) ;
2024-10-31 10:04:54 +01:00
foreach ( ValidationResult validationResult in valueEditor . Validate ( postedValue , isRequired , validationRegExp , validationContext ) )
2020-06-15 23:05:32 +10:00
{
2022-06-07 15:28:38 +02:00
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
if ( isRequired & & ! string . IsNullOrWhiteSpace ( isRequiredMessage ) & &
requiredDefaultMessages . Contains ( validationResult . ErrorMessage , StringComparer . OrdinalIgnoreCase ) )
2020-06-15 23:05:32 +10:00
{
2024-03-04 12:50:24 +01:00
validationResult . ErrorMessage = _textService . UmbracoDictionaryTranslate ( _cultureDictionary , isRequiredMessage ) ;
2020-06-15 23:05:32 +10:00
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
if ( ! string . IsNullOrWhiteSpace ( validationRegExp ) & & ! string . IsNullOrWhiteSpace ( validationRegExpMessage ) & &
formatDefaultMessages . Contains ( validationResult . ErrorMessage , StringComparer . OrdinalIgnoreCase ) )
2019-02-21 14:46:14 +11:00
{
2024-03-04 12:50:24 +01:00
validationResult . ErrorMessage = _textService . UmbracoDictionaryTranslate ( _cultureDictionary , validationRegExpMessage ) ;
2022-06-07 15:28:38 +02:00
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
yield return validationResult ;
}
}
2019-03-28 23:59:49 +11:00
2024-10-31 10:04:54 +01:00
[Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
public IEnumerable < ValidationResult > ValidatePropertyValue (
IDataEditor editor ,
IDataType dataType ,
object? postedValue ,
bool isRequired ,
string? validationRegExp ,
string? isRequiredMessage ,
string? validationRegExpMessage )
= > ValidatePropertyValue ( editor , dataType , postedValue , isRequired , validationRegExp , isRequiredMessage , validationRegExpMessage , PropertyValidationContext . Empty ( ) ) ;
2022-06-07 15:28:38 +02:00
/// <inheritdoc />
public bool IsPropertyDataValid ( IContent content , out IProperty [ ] invalidProperties , CultureImpact ? impact )
{
// select invalid properties
invalidProperties = content . Properties . Where ( x = >
{
var propertyTypeVaries = x . PropertyType . VariesByCulture ( ) ;
2019-03-28 23:59:49 +11:00
2022-06-07 15:28:38 +02:00
if ( impact is null )
{
return false ;
}
2019-03-28 23:59:49 +11:00
2022-06-07 15:28:38 +02:00
// impacts invariant = validate invariant property, invariant culture
if ( impact . ImpactsOnlyInvariantCulture )
{
2024-10-31 10:04:54 +01:00
return ! ( propertyTypeVaries | | IsPropertyValid ( x , PropertyValidationContext . Empty ( ) ) ) ;
2022-06-07 15:28:38 +02:00
}
2019-03-28 23:59:49 +11:00
2022-06-07 15:28:38 +02:00
// impacts all = validate property, all cultures (incl. invariant)
if ( impact . ImpactsAllCultures )
{
2024-10-31 10:04:54 +01:00
return ! IsPropertyValid ( x , PropertyValidationContext . CultureAndSegment ( "*" , null ) ) ;
2022-06-07 15:28:38 +02:00
}
2019-03-28 23:59:49 +11:00
2022-06-07 15:28:38 +02:00
// impacts explicit culture = validate variant property, explicit culture
if ( propertyTypeVaries )
{
2024-10-31 10:04:54 +01:00
return ! IsPropertyValid ( x , PropertyValidationContext . CultureAndSegment ( impact . Culture , null ) ) ;
}
if ( impact . ImpactsExplicitCulture & & GetDataEditor ( x . PropertyType ) ? . CanMergePartialPropertyValues ( x . PropertyType ) is true )
{
return ! IsPropertyValid ( x , new PropertyValidationContext
{
Culture = null ,
Segment = null ,
CulturesBeingValidated = [ impact . Culture ! ] ,
SegmentsBeingValidated = [ ]
} ) ;
2022-06-07 15:28:38 +02:00
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// and, for explicit culture, we may also have to validate invariant property, invariant culture
// if either
// - it is impacted (default culture), or
// - there is no published version of the content - maybe non-default culture, but no published version
var alsoInvariant = impact . ImpactsAlsoInvariantProperties | | ! content . Published ;
2024-10-31 10:04:54 +01:00
return alsoInvariant & & ! IsPropertyValid ( x , PropertyValidationContext . Empty ( ) ) ;
2022-06-07 15:28:38 +02:00
} ) . ToArray ( ) ;
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
return invalidProperties . Length = = 0 ;
}
2019-02-21 14:46:14 +11:00
2024-10-31 10:04:54 +01:00
[Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")]
public bool IsPropertyValid ( IProperty property , string culture = "*" , string segment = "*" )
= > IsPropertyValid ( property , PropertyValidationContext . CultureAndSegment ( culture , segment ) ) ;
2022-06-07 15:28:38 +02:00
/// <inheritdoc />
2024-10-31 10:04:54 +01:00
public bool IsPropertyValid ( IProperty property , PropertyValidationContext validationContext )
2022-06-07 15:28:38 +02:00
{
// NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there.
// The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing.
2024-10-31 10:04:54 +01:00
validationContext = new PropertyValidationContext
{
Culture = validationContext . Culture ? . NullOrWhiteSpaceAsNull ( ) ,
Segment = validationContext . Segment ? . NullOrWhiteSpaceAsNull ( ) ,
CulturesBeingValidated = validationContext . CulturesBeingValidated ,
SegmentsBeingValidated = validationContext . SegmentsBeingValidated
} ;
var culture = validationContext . Culture ;
var segment = validationContext . Segment ;
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
IPropertyValue ? pvalue = null ;
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// if validating invariant/neutral, and it is supported, validate
// (including ensuring that the value exists, if mandatory)
if ( ( culture = = null | | culture = = "*" ) & & ( segment = = null | | segment = = "*" ) & &
property . PropertyType . SupportsVariation ( null , null ) )
{
// validate pvalue (which is the invariant value)
pvalue = property . Values . FirstOrDefault ( x = > x . Culture = = null & & x . Segment = = null ) ;
2024-10-31 10:04:54 +01:00
if ( ! IsValidPropertyValue ( property , pvalue ? . EditedValue , validationContext ) )
2019-02-21 14:46:14 +11:00
{
2022-06-07 15:28:38 +02:00
return false ;
2019-02-21 14:46:14 +11:00
}
2022-06-07 15:28:38 +02:00
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// if validating only invariant/neutral, we are good
if ( culture = = null & & segment = = null )
{
return true ;
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// if nothing else to validate, we are good
if ( ( culture = = null | | culture = = "*" ) & & ( segment = = null | | segment = = "*" ) & &
! property . PropertyType . VariesByCulture ( ) )
{
return true ;
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// for anything else, validate the existing values (including mandatory),
// but we cannot validate mandatory globally (we don't know the possible cultures and segments)
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// validate vvalues (which are the variant values)
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// if we don't have vvalues (property.Values is empty or only contains pvalue), validate null
if ( property . Values . Count = = ( pvalue = = null ? 0 : 1 ) )
{
2024-10-31 10:04:54 +01:00
return culture = = "*" | | IsValidPropertyValue ( property , null , validationContext ) ;
2022-06-07 15:28:38 +02:00
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
// else validate vvalues (but don't revalidate pvalue)
2023-11-22 12:52:08 +01:00
var vvalues = property . Values . Where ( x = >
2022-06-07 15:28:38 +02:00
x ! = pvalue & & // don't revalidate pvalue
property . PropertyType . SupportsVariation ( x . Culture , x . Segment , true ) & & // the value variation is ok
2022-07-11 15:41:10 +02:00
( culture = = "*" | | x . Culture . InvariantEquals ( culture ) ) & & // the culture matches
( segment = = "*" | | x . Segment . InvariantEquals ( segment ) ) ) // the segment matches
2022-06-07 15:28:38 +02:00
. ToList ( ) ;
2019-02-21 14:46:14 +11:00
2023-11-22 12:52:08 +01:00
// if we do not have any vvalues at this point, validate null (no variant values present)
if ( vvalues . Any ( ) is false )
{
2024-10-31 10:04:54 +01:00
return IsValidPropertyValue ( property , null , validationContext ) ;
2023-11-22 12:52:08 +01:00
}
2024-10-31 10:04:54 +01:00
return vvalues . All ( x = > IsValidPropertyValue ( property , x . EditedValue , validationContext ) ) ;
2022-06-07 15:28:38 +02:00
}
2019-02-21 14:46:14 +11:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Boolean indicating whether the passed in value is valid
/// </summary>
/// <param name="property"></param>
/// <param name="value"></param>
2024-11-05 10:26:15 +01:00
/// <param name="validationContext"></param>
2022-06-07 15:28:38 +02:00
/// <returns>True is property value is valid, otherwise false</returns>
2024-10-31 10:04:54 +01:00
private bool IsValidPropertyValue ( IProperty property , object? value , PropertyValidationContext validationContext ) = >
IsPropertyValueValid ( property . PropertyType , value , validationContext ) ;
2022-06-07 15:28:38 +02:00
/// <summary>
/// Determines whether a value is valid for this property type.
/// </summary>
2024-10-31 10:04:54 +01:00
private bool IsPropertyValueValid ( IPropertyType propertyType , object? value , PropertyValidationContext validationContext )
2022-06-07 15:28:38 +02:00
{
2024-10-31 10:04:54 +01:00
IDataEditor ? editor = GetDataEditor ( propertyType ) ;
2022-06-07 15:28:38 +02:00
if ( editor = = null )
2019-02-21 14:46:14 +11:00
{
2022-06-07 15:28:38 +02:00
// nothing much we can do validation wise if the property editor has been removed.
// the property will be displayed as a label, so flagging it as invalid would be pointless.
return true ;
2019-02-21 14:46:14 +11:00
}
2024-10-31 10:04:54 +01:00
var configuration = GetDataType ( propertyType ) ? . ConfigurationObject ;
2022-06-07 15:28:38 +02:00
IDataValueEditor valueEditor = editor . GetValueEditor ( configuration ) ;
2024-10-31 10:04:54 +01:00
return ! valueEditor . Validate ( value , propertyType . Mandatory , propertyType . ValidationRegExp , validationContext ) . Any ( ) ;
2019-02-21 14:46:14 +11:00
}
2024-10-31 10:04:54 +01:00
private IDataType ? GetDataType ( IPropertyType propertyType )
= > _dataTypeService . GetDataType ( propertyType . DataTypeId ) ;
private IDataEditor ? GetDataEditor ( IPropertyType propertyType )
= > _propertyEditors [ propertyType . PropertyEditorAlias ] ;
2019-02-21 14:46:14 +11:00
}