Files
Umbraco-CMS/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs

482 lines
22 KiB
C#
Raw Normal View History

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
2017-09-12 16:22:16 +02:00
using System.Collections.Generic;
using System.Linq;
2020-09-18 14:37:19 +02:00
using Microsoft.Extensions.Logging;
2017-09-12 16:22:16 +02:00
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
2017-09-12 16:22:16 +02:00
namespace Umbraco.Cms.Core.PropertyEditors
2017-09-12 16:22:16 +02:00
{
2018-02-15 14:49:32 +01:00
/// <summary>
/// Represents a nested content property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.NestedContent,
"Nested Content",
"nestedcontent",
ValueType = ValueTypes.Json,
Group = Constants.PropertyEditors.Groups.Lists,
Icon = "icon-thumbnail-list")]
2018-02-25 10:43:16 +01:00
public class NestedContentPropertyEditor : DataEditor
2017-09-12 16:22:16 +02:00
{
2019-12-04 14:03:39 +01:00
private readonly IIOHelper _ioHelper;
2017-09-14 11:41:46 +02:00
public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
2017-09-12 16:22:16 +02:00
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/move-files # Conflicts: # src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs # src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs # src/Umbraco.Tests/Published/NestedContentTests.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs # src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs # src/Umbraco.Tests/Services/ContentServiceTests.cs # src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs # src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs
2019-12-11 08:13:51 +01:00
public NestedContentPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper)
: base (dataValueEditorFactory)
2017-09-12 16:22:16 +02:00
{
2019-12-04 14:03:39 +01:00
_ioHelper = ioHelper;
2017-09-12 16:22:16 +02:00
}
#region Pre Value Editor
2019-12-04 14:03:39 +01:00
protected override IConfigurationEditor CreateConfigurationEditor() => new NestedContentConfigurationEditor(_ioHelper);
2017-09-12 16:22:16 +02:00
#endregion
#region Value Editor
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create<NestedContentPropertyValueEditor>(Attribute);
2017-09-12 16:22:16 +02: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;
private readonly IDataTypeService _dataTypeService;
2020-09-18 14:37:19 +02:00
private readonly ILogger<NestedContentPropertyEditor> _logger;
private readonly NestedContentValues _nestedContentValues;
2017-09-14 11:41:46 +02:00
2020-08-06 12:59:21 +02:00
public NestedContentPropertyValueEditor(
IDataTypeService dataTypeService,
ILocalizedTextService localizedTextService,
IContentTypeService contentTypeService,
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
ILogger<NestedContentPropertyEditor> logger,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
Validators.Add(new NestedContentValidator(propertyValidationService, _nestedContentValues, contentTypeService));
}
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
}
}
#region DB to String
public override string ConvertDbToString(IPropertyType propertyType, object propertyValue)
2017-09-12 16:22:16 +02:00
{
var rows = _nestedContentValues.GetPropertyValues(propertyValue);
2017-09-12 16:22:16 +02:00
if (rows.Count == 0)
return null;
2017-09-12 16:22:16 +02:00
foreach (var row in rows.ToList())
2017-09-12 16:22:16 +02:00
{
foreach(var prop in row.PropertyValues.ToList())
2017-09-12 16:22:16 +02:00
{
try
2017-09-12 16:22:16 +02:00
{
// convert the value, and store the converted value
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null) continue;
var tempConfig = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
var valEditor = propEditor.GetValueEditor(tempConfig);
var convValue = valEditor.ConvertDbToString(prop.Value.PropertyType, prop.Value.Value);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue;
2017-09-12 16:22:16 +02:00
}
catch (InvalidOperationException ex)
2017-09-12 16:22:16 +02:00
{
// deal with weird situations by ignoring them (no comment)
row.RawPropertyValues.Remove(prop.Key);
_logger.LogWarning(
ex,
"ConvertDbToString removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Id, propertyType.Alias);
2017-09-12 16:22:16 +02:00
}
}
}
return JsonConvert.SerializeObject(rows, Formatting.None).ToXmlString<string>();
2017-09-12 16:22:16 +02:00
}
#endregion
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
/// <summary>
/// Ensure that sub-editor values are translated through their ToEditor methods
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
public override object ToEditor(IProperty property, 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);
var valEditors = new Dictionary<int, IDataValueEditor>();
2017-09-12 16:22:16 +02:00
var rows = _nestedContentValues.GetPropertyValues(val);
if (rows.Count == 0)
2017-09-12 16:22:16 +02:00
return string.Empty;
foreach (var row in rows.ToList())
2017-09-12 16:22:16 +02:00
{
foreach(var prop in row.PropertyValues.ToList())
2017-09-12 16:22:16 +02:00
{
try
2017-09-12 16:22:16 +02:00
{
// create a temp property with the value
// - force it to be culture invariant as NC can't handle culture variant element properties
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
var tempProp = new Property(prop.Value.PropertyType);
tempProp.SetValue(prop.Value.Value);
// convert that temp property, and store the converted value
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null)
2017-09-12 16:22:16 +02:00
{
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
continue;
2017-09-12 16:22:16 +02:00
}
var dataTypeId = prop.Value.PropertyType.DataTypeId;
if (!valEditors.TryGetValue(dataTypeId, out var valEditor))
{
Merge remote-tracking branch 'origin/v8/dev' into v9/feature/merge-v8-05072021 # Conflicts: # build/NuSpecs/UmbracoCms.Web.nuspec # src/SolutionInfo.cs # src/Umbraco.Core/Compose/RelateOnTrashComponent.cs # src/Umbraco.Core/Composing/Current.cs # src/Umbraco.Core/Constants-AppSettings.cs # src/Umbraco.Core/Constants-SqlTemplates.cs # src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs # src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs # src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs # src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs # src/Umbraco.Core/Models/IReadOnlyContentBase.cs # src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs # src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs # src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs # src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs # src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs # src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs # src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs # src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs # src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs # src/Umbraco.Core/Routing/UrlProviderExtensions.cs # src/Umbraco.Core/Runtime/CoreRuntime.cs # src/Umbraco.Core/Services/ILocalizedTextService.cs # src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs # src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs # src/Umbraco.Examine/UmbracoContentIndex.cs # src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs # src/Umbraco.Infrastructure/IPublishedContentQuery.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Infrastructure/Models/MediaWithCrops.cs # src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs # src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs # src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs # src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs # src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs # src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs # src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs # src/Umbraco.Infrastructure/PublishedContentQuery.cs # src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs # src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs # src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs # src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs # src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs # src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs # src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs # src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs # src/Umbraco.Tests/App.config # src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs # src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs # src/Umbraco.Tests/PublishedContent/NuCacheTests.cs # src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs # src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs # src/Umbraco.Web.BackOffice/Controllers/ContentController.cs # src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs # src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs # src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs # src/Umbraco.Web.BackOffice/Controllers/MediaController.cs # src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs # src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs # src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs # src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs # src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs # src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs # src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs # src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs # src/Umbraco.Web.Common/Macros/MacroRenderer.cs # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI/Umbraco/config/lang/it.xml # src/Umbraco.Web.UI/web.Template.Debug.config # src/Umbraco.Web.UI/web.Template.config # src/Umbraco.Web/Compose/NotificationsComponent.cs # src/Umbraco.Web/Composing/ModuleInjector.cs # src/Umbraco.Web/Editors/AuthenticationController.cs # src/Umbraco.Web/Editors/BackOfficeController.cs # src/Umbraco.Web/Editors/ContentTypeController.cs # src/Umbraco.Web/Editors/CurrentUserController.cs # src/Umbraco.Web/Editors/DictionaryController.cs # src/Umbraco.Web/Editors/MediaTypeController.cs # src/Umbraco.Web/Editors/MemberController.cs # src/Umbraco.Web/Editors/MemberGroupController.cs # src/Umbraco.Web/Editors/MemberTypeController.cs # src/Umbraco.Web/Editors/NuCacheStatusController.cs # src/Umbraco.Web/Editors/UserGroupsController.cs # src/Umbraco.Web/Editors/UsersController.cs # src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs # src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs # src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs # src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs # src/Umbraco.Web/Models/Trees/MenuItemList.cs # src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataModel.cs # src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs # src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs # src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs # src/Umbraco.Web/Runtime/WebRuntime.cs # src/Umbraco.Web/Search/ExamineComponent.cs # src/Umbraco.Web/Trees/ApplicationTreeController.cs # src/Umbraco.Web/Trees/MemberTreeController.cs # src/Umbraco.Web/UrlHelperRenderExtensions.cs
2021-07-05 20:58:04 +02:00
var tempConfig = _dataTypeService.GetDataType(dataTypeId).Configuration;
valEditor = propEditor.GetValueEditor(tempConfig);
valEditors.Add(dataTypeId, valEditor);
}
var convValue = valEditor.ToEditor(tempProp);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue == null ? null : JToken.FromObject(convValue);
2017-09-12 16:22:16 +02:00
}
catch (InvalidOperationException ex)
2017-09-12 16:22:16 +02:00
{
// deal with weird situations by ignoring them (no comment)
row.RawPropertyValues.Remove(prop.Key);
_logger.LogWarning(
ex,
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Id, property.PropertyType.Alias);
2017-09-12 16:22:16 +02:00
}
}
}
// return the object, there's a native json converter for this so it will serialize correctly
return rows;
2017-09-12 16:22:16 +02:00
}
/// <summary>
/// Ensure that sub-editor values are translated through their FromEditor methods
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
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;
var rows = _nestedContentValues.GetPropertyValues(editorValue.Value);
2017-09-12 16:22:16 +02:00
if (rows.Count == 0)
return null;
2017-09-12 16:22:16 +02:00
foreach (var row in rows.ToList())
2017-09-12 16:22:16 +02:00
{
foreach(var prop in row.PropertyValues.ToList())
2017-09-12 16:22:16 +02:00
{
// Fetch the property types prevalue
var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
2017-09-12 16:22:16 +02:00
// Lookup the property editor
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null) continue;
2017-09-12 16:22:16 +02:00
// Create a fake content property data object
var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration);
2017-09-12 16:22:16 +02:00
// Get the property editor to do it's conversion
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value);
2017-09-12 16:22:16 +02:00
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = (newValue == null) ? null : JToken.FromObject(newValue);
2017-09-12 16:22:16 +02:00
}
}
// return json
return JsonConvert.SerializeObject(rows, Formatting.None);
2017-09-12 16:22:16 +02:00
}
#endregion
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))
{
foreach(var prop in row.PropertyValues)
{
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
var valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference)) continue;
var val = prop.Value.Value?.ToString();
var refs = reference.GetReferences(val);
result.AddRange(refs);
}
}
return result;
}
2017-09-12 16:22:16 +02:00
}
/// <summary>
/// Validator for nested content to ensure that all nesting of editors is validated
/// </summary>
internal class NestedContentValidator : ComplexEditorValidator
2017-09-12 16:22:16 +02:00
{
private readonly NestedContentValues _nestedContentValues;
private readonly IContentTypeService _contentTypeService;
2017-09-14 11:41:46 +02:00
public NestedContentValidator(IPropertyValidationService propertyValidationService, NestedContentValues nestedContentValues, IContentTypeService contentTypeService)
: base(propertyValidationService)
2017-09-14 11:41:46 +02:00
{
_nestedContentValues = nestedContentValues;
_contentTypeService = contentTypeService;
2017-09-14 11:41:46 +02:00
}
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
2017-09-12 16:22:16 +02:00
{
var rows = _nestedContentValues.GetPropertyValues(value);
if (rows.Count == 0) yield break;
// There is no guarantee that the client will post data for every property defined in the Element Type but we still
// need to validate that data for each property especially for things like 'required' data to work.
// Lookup all element types for all content/settings and then we can populate any empty properties.
var allElementAliases = rows.Select(x => x.ContentTypeAlias).ToList();
// unfortunately we need to get all content types and post filter - but they are cached so its ok, there's
// no overload to lookup by many aliases.
var allElementTypes = _contentTypeService.GetAll().Where(x => allElementAliases.Contains(x.Alias)).ToDictionary(x => x.Alias);
foreach (var row in rows)
2017-09-12 16:22:16 +02:00
{
if (!allElementTypes.TryGetValue(row.ContentTypeAlias, out var elementType))
throw new InvalidOperationException($"No element type found with alias {row.ContentTypeAlias}");
// now ensure missing properties
foreach (var elementTypeProp in elementType.CompositionPropertyTypes)
{
if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias))
{
// set values to null
row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue
{
PropertyType = elementTypeProp,
Value = null
};
row.RawPropertyValues[elementTypeProp.Alias] = null;
}
}
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Id);
foreach (var prop in row.PropertyValues)
2017-09-12 16:22:16 +02:00
{
elementValidation.AddPropertyTypeValidation(
2020-08-06 12:59:21 +02:00
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
2017-09-12 16:22:16 +02:00
}
yield return elementValidation;
}
2017-09-12 16:22:16 +02:00
}
}
/// <summary>
/// Used to deserialize the nested content serialized value
/// </summary>
internal class NestedContentValues
{
private readonly Lazy<Dictionary<string, IContentType>> _contentTypes;
2017-09-12 16:22:16 +02:00
public NestedContentValues(IContentTypeService contentTypeService)
{
_contentTypes = new Lazy<Dictionary<string, IContentType>>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias));
}
2017-09-12 16:22:16 +02:00
private IContentType GetElementType(NestedContentRowValue item)
{
_contentTypes.Value.TryGetValue(item.ContentTypeAlias, out var contentType);
return contentType;
}
/// <summary>
/// Deserialize the raw json property value
/// </summary>
/// <param name="propertyValue"></param>
/// <returns></returns>
public IReadOnlyList<NestedContentRowValue> GetPropertyValues(object propertyValue)
{
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
return new List<NestedContentRowValue>();
if (!propertyValue.ToString().DetectIsJson())
return new List<NestedContentRowValue>();
Merge remote-tracking branch 'origin/v8/dev' into netcore/dev # Conflicts: # .github/workflows/codeql-analysis.yml # src/Umbraco.Core/CompositionExtensions.cs # src/Umbraco.Core/Constants-AppSettings.cs # src/Umbraco.Core/GuidUdi.cs # src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs # src/Umbraco.Core/PublishedContentExtensions.cs # src/Umbraco.Core/Routing/DefaultUrlProvider.cs # src/Umbraco.Core/Routing/UrlProvider.cs # src/Umbraco.Core/Routing/UrlProviderExtensions.cs # src/Umbraco.Core/Runtime/MainDom.cs # src/Umbraco.Core/Services/IRuntimeState.cs # src/Umbraco.Core/StringExtensions.cs # src/Umbraco.Core/Sync/ApplicationUrlHelper.cs # src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs # src/Umbraco.Core/Sync/IServerRegistrar.cs # src/Umbraco.Infrastructure/Media/EmbedProviders/Instagram.cs # src/Umbraco.Infrastructure/Models/ContentBaseExtensions.cs # src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs # src/Umbraco.Tests/Misc/ApplicationUrlHelperTests.cs # src/Umbraco.Tests/Routing/UrlProviderTests.cs # src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs # src/Umbraco.Tests/TestHelpers/TestObjects.cs # src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml # src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml # src/Umbraco.Web/CompositionExtensions.cs # src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs # src/Umbraco.Web/ImageCropperTemplateExtensions.cs # src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PublishedContentExtensions.cs # src/Umbraco.Web/Runtime/WebInitialComponent.cs # src/Umbraco.Web/Runtime/WebInitialComposer.cs # src/Umbraco.Web/Templates/TemplateUtilities.cs # src/Umbraco.Web/UmbracoContext.cs # src/Umbraco.Web/UmbracoHelper.cs # src/Umbraco.Web/UmbracoInjectedModule.cs
2020-11-30 11:24:24 +01:00
var rowValues = JsonConvert.DeserializeObject<List<NestedContentRowValue>>(propertyValue.ToString());
// 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.
if (rowValues == null || rowValues.Count == 0)
return new List<NestedContentRowValue>();
2020-08-06 12:59:21 +02:00
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
foreach (var row in rowValues)
{
var contentType = GetElementType(row);
if (contentType == null)
continue;
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
// objects on each iteration.
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
// find any keys that are not real property types and remove them
if (row.RawPropertyValues != null)
{
foreach (var prop in row.RawPropertyValues.ToList())
{
if (IsSystemPropertyKey(prop.Key)) continue;
// doesn't exist so remove it
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
{
row.RawPropertyValues.Remove(prop.Key);
}
else
{
// set the value to include the resolved property type
row.PropertyValues[prop.Key] = new NestedContentPropertyValue
{
PropertyType = propType,
Value = prop.Value
};
}
}
2017-09-12 16:22:16 +02:00
}
}
return rowValues;
}
/// <summary>
/// Used during deserialization to populate the property value/property type of a nested content row property
/// </summary>
internal class NestedContentPropertyValue
{
public object Value { get; set; }
2020-08-06 12:59:21 +02:00
public IPropertyType PropertyType { get; set; }
}
/// <summary>
/// Used to deserialize a nested content row
/// </summary>
internal class NestedContentRowValue
{
[JsonProperty("key")]
public Guid Id{ get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("ncContentTypeAlias")]
public string ContentTypeAlias { get; set; }
public IPropertyType PropType { get; }
/// <summary>
/// The remaining properties will be serialized to a dictionary
/// </summary>
/// <remarks>
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
/// NestedContent serializes to string, int, whatever eg
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
/// </remarks>
[JsonExtensionData]
public IDictionary<string, object> RawPropertyValues { get; set; }
/// <summary>
/// Used during deserialization to convert the raw property data into data with a property type context
/// </summary>
[JsonIgnore]
public IDictionary<string, NestedContentPropertyValue> PropertyValues { get; set; } = new Dictionary<string, NestedContentPropertyValue>();
2017-09-12 16:22:16 +02:00
}
}
#endregion
private static bool IsSystemPropertyKey(string propKey)
{
return propKey == "name" || propKey == "key" || propKey == ContentTypeAliasPropertyKey;
}
}
}