2017-09-12 16:22:16 +02:00
using System ;
2019-11-01 14:22:49 +11:00
using System.Collections ;
2017-09-12 16:22:16 +02:00
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
using System.Text.RegularExpressions ;
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
using Umbraco.Core ;
using Umbraco.Core.Composing ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Models ;
using Umbraco.Core.Models.Editors ;
using Umbraco.Core.PropertyEditors ;
using Umbraco.Core.Services ;
namespace Umbraco.Web.PropertyEditors
{
2018-02-15 14:49:32 +01:00
/// <summary>
/// Represents a nested content property editor.
/// </summary>
2019-06-07 17:59:38 +01:00
[ DataEditor (
Constants . PropertyEditors . Aliases . NestedContent ,
"Nested Content" ,
"nestedcontent" ,
ValueType = ValueTypes . Json ,
Group = Constants . PropertyEditors . Groups . Lists ,
2019-10-02 10:41:00 +02:00
Icon = "icon-thumbnail-list" ) ]
2018-02-25 10:43:16 +01:00
public class NestedContentPropertyEditor : DataEditor
2017-09-12 16:22:16 +02:00
{
2017-09-20 20:06:46 +02:00
private readonly Lazy < PropertyEditorCollection > _propertyEditors ;
2019-10-30 17:14:04 +11:00
private readonly IDataTypeService _dataTypeService ;
2019-11-01 14:22:49 +11:00
private readonly IContentTypeService _contentTypeService ;
2017-09-12 16:22:16 +02:00
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias" ;
2019-11-01 14:22:49 +11:00
public NestedContentPropertyEditor ( ILogger logger , Lazy < PropertyEditorCollection > propertyEditors , IDataTypeService dataTypeService , IContentTypeService contentTypeService )
2017-09-12 16:22:16 +02:00
: base ( logger )
{
2017-09-14 11:41:46 +02:00
_propertyEditors = propertyEditors ;
2019-10-30 17:14:04 +11:00
_dataTypeService = dataTypeService ;
2019-11-01 14:22:49 +11:00
_contentTypeService = contentTypeService ;
2017-09-12 16:22:16 +02:00
}
2017-09-20 20:06:46 +02:00
// has to be lazy else circular dep in ctor
private PropertyEditorCollection PropertyEditors = > _propertyEditors . Value ;
2017-09-12 16:22:16 +02:00
#region Pre Value Editor
2018-02-15 14:49:32 +01:00
protected override IConfigurationEditor CreateConfigurationEditor ( ) = > new NestedContentConfigurationEditor ( ) ;
2017-09-12 16:22:16 +02:00
#endregion
#region Value Editor
2019-11-01 14:22:49 +11:00
protected override IDataValueEditor CreateValueEditor ( ) = > new NestedContentPropertyValueEditor ( Attribute , PropertyEditors , _dataTypeService , _contentTypeService ) ;
2017-09-12 16:22:16 +02:00
2019-12-02 15:00:56 +00:00
internal class NestedContentPropertyValueEditor : DataValueEditor , IDataValueReference
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
private readonly PropertyEditorCollection _propertyEditors ;
2019-10-30 17:14:04 +11:00
private readonly IDataTypeService _dataTypeService ;
2019-11-01 14:22:49 +11:00
private readonly NestedContentValues _nestedContentValues ;
public NestedContentPropertyValueEditor ( DataEditorAttribute attribute , PropertyEditorCollection propertyEditors , IDataTypeService dataTypeService , IContentTypeService contentTypeService )
2018-01-24 13:37:14 +01:00
: base ( attribute )
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
_propertyEditors = propertyEditors ;
2019-10-30 17:14:04 +11:00
_dataTypeService = dataTypeService ;
2019-11-01 14:22:49 +11:00
_nestedContentValues = new NestedContentValues ( contentTypeService ) ;
Validators . Add ( new NestedContentValidator ( propertyEditors , dataTypeService , _nestedContentValues ) ) ;
2017-09-12 16:22:16 +02:00
}
2018-01-26 17:55:20 +01:00
/// <inheritdoc />
public override object Configuration
2017-09-12 16:22:16 +02:00
{
2018-01-26 17:55:20 +01:00
get = > base . Configuration ;
set
2017-09-12 16:22:16 +02:00
{
2018-01-26 17:55:20 +01:00
if ( value = = null )
throw new ArgumentNullException ( nameof ( value ) ) ;
if ( ! ( value is NestedContentConfiguration configuration ) )
2018-03-16 09:06:44 +01:00
throw new ArgumentException ( $"Expected a {typeof(NestedContentConfiguration).Name} instance, but got {value.GetType().Name}." , nameof ( value ) ) ;
2018-01-26 17:55:20 +01:00
base . Configuration = value ;
2018-03-16 09:06:44 +01:00
HideLabel = configuration . HideLabel . TryConvertTo < bool > ( ) . Result ;
2017-09-12 16:22:16 +02:00
}
}
2019-10-30 17:14:04 +11:00
#region DB to String
public override string ConvertDbToString ( PropertyType propertyType , object propertyValue , IDataTypeService dataTypeService )
{
2019-11-01 14:45:05 +11:00
var vals = _nestedContentValues . GetPropertyValues ( propertyValue , out var deserialized ) . ToList ( ) ;
2019-11-01 14:22:49 +11:00
if ( vals . Count = = 0 )
return string . Empty ;
foreach ( var row in vals )
2019-10-30 17:14:04 +11:00
{
2019-11-01 14:22:49 +11:00
if ( row . PropType = = null )
2019-10-30 17:14:04 +11:00
{
// type not found, and property is not system: just delete the value
2019-11-01 14:22:49 +11:00
if ( IsSystemPropertyKey ( row . PropKey ) = = false )
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = null ;
2019-10-30 17:14:04 +11:00
}
else
{
try
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
// convert the value, and store the converted value
2019-11-01 14:22:49 +11:00
var propEditor = _propertyEditors [ row . PropType . PropertyEditorAlias ] ;
if ( propEditor = = null ) continue ;
var tempConfig = dataTypeService . GetDataType ( row . PropType . DataTypeId ) . Configuration ;
2019-10-30 17:14:04 +11:00
var valEditor = propEditor . GetValueEditor ( tempConfig ) ;
2019-11-01 14:45:05 +11:00
var convValue = valEditor . ConvertDbToString ( row . PropType , row . JsonRowValue [ row . PropKey ] ? . ToString ( ) , dataTypeService ) ;
row . JsonRowValue [ row . PropKey ] = convValue ;
2017-09-12 16:22:16 +02:00
}
2019-10-30 17:14:04 +11:00
catch ( InvalidOperationException )
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
// deal with weird situations by ignoring them (no comment)
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = null ;
2017-09-12 16:22:16 +02:00
}
}
2019-11-01 14:22:49 +11:00
}
2017-09-12 16:22:16 +02:00
2019-11-01 14:45:05 +11:00
return JsonConvert . SerializeObject ( deserialized ) . ToXmlString < string > ( ) ;
2017-09-12 16:22:16 +02:00
}
#endregion
2019-10-30 17:14:04 +11:00
2017-11-15 08:53:20 +01:00
#region Convert database // editor
// note: there is NO variant support here
2017-09-12 16:22:16 +02:00
2018-04-21 09:57:28 +02:00
public override object ToEditor ( Property property , IDataTypeService dataTypeService , string culture = null , string segment = null )
2017-09-12 16:22:16 +02:00
{
2018-04-21 09:57:28 +02:00
var val = property . GetValue ( culture , segment ) ;
2017-09-12 16:22:16 +02:00
2019-11-01 14:45:05 +11:00
var vals = _nestedContentValues . GetPropertyValues ( val , out var deserialized ) . ToList ( ) ;
2019-11-01 14:22:49 +11:00
if ( vals . Count = = 0 )
return string . Empty ;
foreach ( var row in vals )
2017-09-12 16:22:16 +02:00
{
2019-11-01 14:22:49 +11:00
if ( row . PropType = = null )
2019-10-30 17:14:04 +11:00
{
// type not found, and property is not system: just delete the value
2019-11-01 14:22:49 +11:00
if ( IsSystemPropertyKey ( row . PropKey ) = = false )
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = null ;
2019-10-30 17:14:04 +11:00
}
else
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
try
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
// create a temp property with the value
// - force it to be culture invariant as NC can't handle culture variant element properties
2019-11-01 14:22:49 +11:00
row . PropType . Variations = ContentVariation . Nothing ;
var tempProp = new Property ( row . PropType ) ;
2019-11-01 14:45:05 +11:00
tempProp . SetValue ( row . JsonRowValue [ row . PropKey ] = = null ? null : row . JsonRowValue [ row . PropKey ] . ToString ( ) ) ;
2019-10-30 17:14:04 +11:00
// convert that temp property, and store the converted value
2019-11-01 14:22:49 +11:00
var propEditor = _propertyEditors [ row . PropType . PropertyEditorAlias ] ;
if ( propEditor = = null )
{
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = tempProp . GetValue ( ) ? . ToString ( ) ;
2019-11-01 14:22:49 +11:00
continue ;
}
var tempConfig = dataTypeService . GetDataType ( row . PropType . DataTypeId ) . Configuration ;
2019-10-30 17:14:04 +11:00
var valEditor = propEditor . GetValueEditor ( tempConfig ) ;
var convValue = valEditor . ToEditor ( tempProp , dataTypeService ) ;
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = convValue = = null ? null : JToken . FromObject ( convValue ) ;
2017-09-12 16:22:16 +02:00
}
2019-10-30 17:14:04 +11:00
catch ( InvalidOperationException )
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
// deal with weird situations by ignoring them (no comment)
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = null ;
2017-09-12 16:22:16 +02:00
}
}
2019-11-01 14:22:49 +11:00
}
2017-09-12 16:22:16 +02:00
2017-11-15 08:53:20 +01:00
// return json
2019-11-01 14:45:05 +11:00
return deserialized ;
2017-09-12 16:22:16 +02:00
}
2018-03-16 09:06:44 +01:00
public override object FromEditor ( ContentPropertyData editorValue , object currentValue )
2017-09-12 16:22:16 +02:00
{
if ( editorValue . Value = = null | | string . IsNullOrWhiteSpace ( editorValue . Value . ToString ( ) ) )
return null ;
2019-11-01 14:45:05 +11:00
var vals = _nestedContentValues . GetPropertyValues ( editorValue . Value , out var deserialized ) . ToList ( ) ;
2019-11-01 14:22:49 +11:00
if ( vals . Count = = 0 )
return string . Empty ;
foreach ( var row in vals )
2017-09-12 16:22:16 +02:00
{
2019-11-01 14:22:49 +11:00
if ( row . PropType = = null )
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
// type not found, and property is not system: just delete the value
2019-11-01 14:22:49 +11:00
if ( IsSystemPropertyKey ( row . PropKey ) = = false )
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = null ;
2017-09-12 16:22:16 +02:00
}
2019-10-30 17:14:04 +11:00
else
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
// Fetch the property types prevalue
2019-11-01 14:22:49 +11:00
var propConfiguration = _dataTypeService . GetDataType ( row . PropType . DataTypeId ) . Configuration ;
2017-09-12 16:22:16 +02:00
2019-10-30 17:14:04 +11:00
// Lookup the property editor
2019-11-01 14:22:49 +11:00
var propEditor = _propertyEditors [ row . PropType . PropertyEditorAlias ] ;
if ( propEditor = = null ) continue ;
2017-09-12 16:22:16 +02:00
2019-10-30 17:14:04 +11:00
// Create a fake content property data object
2019-11-01 14:45:05 +11:00
var contentPropData = new ContentPropertyData ( row . JsonRowValue [ row . PropKey ] , propConfiguration ) ;
2017-09-12 16:22:16 +02:00
2019-10-30 17:14:04 +11:00
// Get the property editor to do it's conversion
2019-11-01 14:45:05 +11:00
var newValue = propEditor . GetValueEditor ( ) . FromEditor ( contentPropData , row . JsonRowValue [ row . PropKey ] ) ;
2017-09-12 16:22:16 +02:00
2019-10-30 17:14:04 +11:00
// Store the value back
2019-11-01 14:45:05 +11:00
row . JsonRowValue [ row . PropKey ] = ( newValue = = null ) ? null : JToken . FromObject ( newValue ) ;
2017-09-12 16:22:16 +02:00
}
2019-11-01 14:22:49 +11:00
}
2019-10-30 17:14:04 +11:00
// return json
2019-11-01 14:45:05 +11:00
return JsonConvert . SerializeObject ( deserialized ) ;
2017-09-12 16:22:16 +02:00
}
#endregion
2019-12-02 15:00:56 +00:00
public IEnumerable < UmbracoEntityReference > GetReferences ( object value )
{
var rawJson = value = = null ? string . Empty : value is string str ? str : value . ToString ( ) ;
var result = new List < UmbracoEntityReference > ( ) ;
foreach ( var row in _nestedContentValues . GetPropertyValues ( rawJson , out _ ) )
{
if ( row . PropType = = null ) continue ;
var propEditor = _propertyEditors [ row . PropType . PropertyEditorAlias ] ;
var valueEditor = propEditor ? . GetValueEditor ( ) ;
if ( ! ( valueEditor is IDataValueReference reference ) ) continue ;
var val = row . JsonRowValue [ row . PropKey ] ? . ToString ( ) ;
var refs = reference . GetReferences ( val ) ;
result . AddRange ( refs ) ;
}
return result ;
}
2017-09-12 16:22:16 +02:00
}
2018-01-24 11:44:44 +01:00
internal class NestedContentValidator : IValueValidator
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
private readonly PropertyEditorCollection _propertyEditors ;
2019-10-30 17:14:04 +11:00
private readonly IDataTypeService _dataTypeService ;
2019-11-01 14:22:49 +11:00
private readonly NestedContentValues _nestedContentValues ;
2017-09-14 11:41:46 +02:00
2019-11-01 14:22:49 +11:00
public NestedContentValidator ( PropertyEditorCollection propertyEditors , IDataTypeService dataTypeService , NestedContentValues nestedContentValues )
2017-09-14 11:41:46 +02:00
{
_propertyEditors = propertyEditors ;
2019-10-30 17:14:04 +11:00
_dataTypeService = dataTypeService ;
2019-11-01 14:22:49 +11:00
_nestedContentValues = nestedContentValues ;
2017-09-14 11:41:46 +02:00
}
2018-01-24 11:44:44 +01:00
public IEnumerable < ValidationResult > Validate ( object rawValue , string valueType , object dataTypeConfiguration )
2017-09-12 16:22:16 +02:00
{
2019-10-30 17:14:04 +11:00
var validationResults = new List < ValidationResult > ( ) ;
2018-09-30 19:09:06 +02:00
2019-11-01 14:45:05 +11:00
foreach ( var row in _nestedContentValues . GetPropertyValues ( rawValue , out _ ) )
2017-09-12 16:22:16 +02:00
{
2019-11-01 14:22:49 +11:00
if ( row . PropType = = null ) continue ;
2017-09-12 16:22:16 +02:00
2019-11-01 14:22:49 +11:00
var config = _dataTypeService . GetDataType ( row . PropType . DataTypeId ) . Configuration ;
var propertyEditor = _propertyEditors [ row . PropType . PropertyEditorAlias ] ;
if ( propertyEditor = = null ) continue ;
2019-10-30 17:14:04 +11:00
foreach ( var validator in propertyEditor . GetValueEditor ( ) . Validators )
{
2019-11-01 14:45:05 +11:00
foreach ( var result in validator . Validate ( row . JsonRowValue [ row . PropKey ] , propertyEditor . GetValueEditor ( ) . ValueType , config ) )
2019-10-30 17:14:04 +11:00
{
2019-11-01 14:45:05 +11:00
result . ErrorMessage = "Item " + ( row . RowIndex + 1 ) + " '" + row . PropType . Name + "' " + result . ErrorMessage ;
2019-10-30 17:14:04 +11:00
validationResults . Add ( result ) ;
}
}
2017-09-12 16:22:16 +02:00
2019-10-30 17:14:04 +11:00
// Check mandatory
2019-11-01 14:22:49 +11:00
if ( row . PropType . Mandatory )
2019-10-30 17:14:04 +11:00
{
2019-11-01 14:45:05 +11:00
if ( row . JsonRowValue [ row . PropKey ] = = null )
validationResults . Add ( new ValidationResult ( "Item " + ( row . RowIndex + 1 ) + " '" + row . PropType . Name + "' cannot be null" , new [ ] { row . PropKey } ) ) ;
else if ( row . JsonRowValue [ row . PropKey ] . ToString ( ) . IsNullOrWhiteSpace ( ) | | ( row . JsonRowValue [ row . PropKey ] . Type = = JTokenType . Array & & ! row . JsonRowValue [ row . PropKey ] . HasValues ) )
validationResults . Add ( new ValidationResult ( "Item " + ( row . RowIndex + 1 ) + " '" + row . PropType . Name + "' cannot be empty" , new [ ] { row . PropKey } ) ) ;
2019-10-30 17:14:04 +11:00
}
2017-09-12 16:22:16 +02:00
2019-10-30 17:14:04 +11:00
// Check regex
2019-11-01 14:22:49 +11:00
if ( ! row . PropType . ValidationRegExp . IsNullOrWhiteSpace ( )
2019-11-01 14:45:05 +11:00
& & row . JsonRowValue [ row . PropKey ] ! = null & & ! row . JsonRowValue [ row . PropKey ] . ToString ( ) . IsNullOrWhiteSpace ( ) )
2017-09-12 16:22:16 +02:00
{
2019-11-01 14:22:49 +11:00
var regex = new Regex ( row . PropType . ValidationRegExp ) ;
2019-11-01 14:45:05 +11:00
if ( ! regex . IsMatch ( row . JsonRowValue [ row . PropKey ] . ToString ( ) ) )
2017-09-12 16:22:16 +02:00
{
2019-11-01 14:45:05 +11:00
validationResults . Add ( new ValidationResult ( "Item " + ( row . RowIndex + 1 ) + " '" + row . PropType . Name + "' is invalid, it does not match the correct pattern" , new [ ] { row . PropKey } ) ) ;
2017-09-12 16:22:16 +02:00
}
}
2019-11-01 14:22:49 +11:00
}
2019-10-30 17:14:04 +11:00
return validationResults ;
2017-09-12 16:22:16 +02:00
}
}
2019-11-01 14:22:49 +11:00
internal class NestedContentValues
{
private readonly Lazy < Dictionary < string , IContentType > > _contentTypes ;
public NestedContentValues ( IContentTypeService contentTypeService )
{
_contentTypes = new Lazy < Dictionary < string , IContentType > > ( ( ) = > contentTypeService . GetAll ( ) . ToDictionary ( c = > c . Alias ) ) ;
}
private IContentType GetElementType ( JObject item )
{
var contentTypeAlias = item [ ContentTypeAliasPropertyKey ] ? . ToObject < string > ( ) ? ? string . Empty ;
_contentTypes . Value . TryGetValue ( contentTypeAlias , out var contentType ) ;
return contentType ;
}
2019-11-01 14:45:05 +11:00
public IEnumerable < RowValue > GetPropertyValues ( object propertyValue , out List < JObject > deserialized )
2019-11-01 14:22:49 +11:00
{
2019-11-01 14:45:05 +11:00
var rowValues = new List < RowValue > ( ) ;
deserialized = null ;
2019-11-01 14:22:49 +11:00
if ( propertyValue = = null | | string . IsNullOrWhiteSpace ( propertyValue . ToString ( ) ) )
2019-11-01 14:45:05 +11:00
return Enumerable . Empty < RowValue > ( ) ;
2019-11-01 14:22:49 +11:00
2019-11-01 14:45:05 +11:00
deserialized = JsonConvert . DeserializeObject < List < JObject > > ( propertyValue . ToString ( ) ) ;
2019-11-01 14:22:49 +11:00
// There was a note here about checking if the result had zero items and if so it would return null, so we'll continue to do that
// The original note was: "Issue #38 - Keep recursive property lookups working"
// Which is from the original NC tracker: https://github.com/umco/umbraco-nested-content/issues/38
// This check should be used everywhere when iterating NC prop values, instead of just the one previous place so that
// empty values don't get persisted when there is nothing, it should actually be null.
2019-11-01 14:45:05 +11:00
if ( deserialized = = null | | deserialized . Count = = 0 )
return Enumerable . Empty < RowValue > ( ) ;
2019-11-01 14:22:49 +11:00
var index = 0 ;
2019-11-01 14:45:05 +11:00
foreach ( var o in deserialized )
2019-11-01 14:22:49 +11:00
{
var propValues = o ;
var contentType = GetElementType ( propValues ) ;
if ( contentType = = null )
continue ;
var propertyTypes = contentType . CompositionPropertyTypes . ToDictionary ( x = > x . Alias , x = > x ) ;
var propAliases = propValues . Properties ( ) . Select ( x = > x . Name ) ;
foreach ( var propAlias in propAliases )
{
propertyTypes . TryGetValue ( propAlias , out var propType ) ;
2019-11-01 14:45:05 +11:00
rowValues . Add ( new RowValue ( propAlias , propType , propValues , index ) ) ;
2019-11-01 14:22:49 +11:00
}
index + + ;
}
2019-11-01 14:45:05 +11:00
return rowValues ;
2019-11-01 14:22:49 +11:00
}
internal class RowValue
{
public RowValue ( string propKey , PropertyType propType , JObject propValues , int index )
{
PropKey = propKey ? ? throw new ArgumentNullException ( nameof ( propKey ) ) ;
2019-11-01 14:45:05 +11:00
PropType = propType ;
JsonRowValue = propValues ? ? throw new ArgumentNullException ( nameof ( propValues ) ) ;
RowIndex = index ;
2019-11-01 14:22:49 +11:00
}
2019-11-01 14:45:05 +11:00
/// <summary>
/// The current property key being iterated for the row value
/// </summary>
2019-11-01 14:22:49 +11:00
public string PropKey { get ; }
2019-11-01 14:45:05 +11:00
/// <summary>
/// The <see cref="PropertyType"/> of the value (if any), this may be null
/// </summary>
2019-11-01 14:22:49 +11:00
public PropertyType PropType { get ; }
2019-11-01 14:45:05 +11:00
/// <summary>
/// The json values for the current row
/// </summary>
public JObject JsonRowValue { get ; }
/// <summary>
/// The Nested Content row index
/// </summary>
public int RowIndex { get ; }
2019-11-01 14:22:49 +11:00
}
}
2017-09-12 16:22:16 +02:00
#endregion
private static bool IsSystemPropertyKey ( string propKey )
{
return propKey = = "name" | | propKey = = "key" | | propKey = = ContentTypeAliasPropertyKey ;
}
}
}