using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; internal abstract class BlockValuePropertyIndexValueFactoryBase : JsonPropertyIndexValueFactoryBase { private readonly PropertyEditorCollection _propertyEditorCollection; protected BlockValuePropertyIndexValueFactoryBase( PropertyEditorCollection propertyEditorCollection, IJsonSerializer jsonSerializer, IOptionsMonitor indexingSettings) : base(jsonSerializer, indexingSettings) { _propertyEditorCollection = propertyEditorCollection; } protected override IEnumerable Handle( TSerialized deserializedPropertyValue, IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures, IDictionary contentTypeDictionary) { var result = new List(); var index = 0; foreach (RawDataItem rawData in GetDataItems(deserializedPropertyValue, published)) { if (contentTypeDictionary.TryGetValue(rawData.ContentTypeKey, out IContentType? contentType) is false) { continue; } var propertyTypeDictionary = contentType .CompositionPropertyTypes .Select(propertyType => { // We want to ensure that the nested properties are set vary by culture if the parent is // This is because it's perfectly valid to have a nested property type that's set to invariant even if the parent varies. // For instance in a block list, the list it self can vary, but the elements can be invariant, at the same time. if (culture is not null) { propertyType.Variations |= ContentVariation.Culture; } if (segment is not null) { propertyType.Variations |= ContentVariation.Segment; } return propertyType; }) .ToDictionary(x => x.Alias); result.AddRange(GetNestedResults( $"{property.Alias}.items[{index}]", culture, segment, published, propertyTypeDictionary, rawData, availableCultures, contentTypeDictionary)); index++; } return RenameKeysToEnsureRawSegmentsIsAPrefix(result); } /// /// Rename keys that count the RAW-constant, to ensure the RAW-constant is a prefix. /// private IEnumerable RenameKeysToEnsureRawSegmentsIsAPrefix( List indexContent) { foreach (IndexValue indexValue in indexContent) { // Tests if key includes the RawFieldPrefix and it is not in the start if (indexValue.FieldName.Substring(1).Contains(UmbracoExamineFieldNames.RawFieldPrefix)) { indexValue.FieldName = UmbracoExamineFieldNames.RawFieldPrefix + indexValue.FieldName.Replace(UmbracoExamineFieldNames.RawFieldPrefix, string.Empty); } } return indexContent; } /// /// Get the data items of a parent item. E.g. block list have contentData. /// protected abstract IEnumerable GetDataItems(TSerialized input, bool published); /// /// Unwraps block item data as data items. /// protected IEnumerable GetDataItems(IList contentData, IList expose, bool published) { if (published is false) { return contentData.Select(ToRawData); } var indexData = new List(); foreach (BlockItemData blockItemData in contentData) { var exposedCultures = expose .Where(e => e.ContentKey == blockItemData.Key) .Select(e => e.Culture) .ToArray(); if (exposedCultures.Any() is false) { continue; } if (exposedCultures.Contains(null) || exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture))) { indexData.Add(ToRawData(blockItemData)); continue; } indexData.Add( ToRawData( blockItemData.ContentTypeKey, blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture)))); } return indexData; } /// /// Index a key with the name of the property, using the relevant content of all the children. /// protected override IEnumerable HandleResume( List indexedContent, IProperty property, string? culture, string? segment, bool published) { var indexedCultures = indexedContent .DistinctBy(v => v.Culture) .Select(v => v.Culture) .WhereNotNull() .ToArray(); var cultures = indexedCultures.Any() ? indexedCultures : new string?[] { culture }; return cultures.Select(c => new IndexValue { Culture = c, FieldName = property.Alias, Values = [GetResumeFromAllContent(indexedContent, c)] }); } /// /// Gets a resume as string of all the content in this nested type. /// /// All the indexed content for this property. /// The culture to get the resume for. /// the string with all relevant content from private static string GetResumeFromAllContent(List indexedContent, string? culture) { var stringBuilder = new StringBuilder(); foreach (IndexValue indexValue in indexedContent.Where(v => v.Culture == culture || v.Culture is null)) { // Ignore Raw fields if (indexValue.FieldName.Contains(UmbracoExamineFieldNames.RawFieldPrefix)) { continue; } foreach (var value in indexValue.Values) { if (value is not null) { stringBuilder.AppendLine(value.ToString()); } } } return stringBuilder.ToString(); } /// /// Gets the content to index for the nested type. E.g. Block list, Nested Content, etc.. /// private IEnumerable GetNestedResults( string keyPrefix, string? culture, string? segment, bool published, IDictionary propertyTypeDictionary, RawDataItem rawData, IEnumerable availableCultures, IDictionary contentTypeDictionary) { foreach (RawPropertyData rawPropertyData in rawData.Properties) { if (propertyTypeDictionary.TryGetValue(rawPropertyData.Alias, out IPropertyType? propertyType)) { IDataEditor? editor = _propertyEditorCollection[propertyType.PropertyEditorAlias]; if (editor is null) { continue; } IProperty subProperty = new Property(propertyType); IEnumerable indexValues = null!; var propertyCulture = rawPropertyData.Culture ?? culture; if (propertyType.VariesByCulture() && propertyCulture is null) { foreach (var availableCulture in availableCultures) { subProperty.SetValue(rawPropertyData.Value, availableCulture, segment); if (published) { subProperty.PublishValues(availableCulture, segment ?? "*"); } indexValues = editor.PropertyIndexValueFactory.GetIndexValues(subProperty, availableCulture, segment, published, availableCultures, contentTypeDictionary); } } else { subProperty.SetValue(rawPropertyData.Value, propertyCulture, segment); if (published) { subProperty.PublishValues(propertyCulture ?? "*", segment ?? "*"); } indexValues = editor.PropertyIndexValueFactory.GetIndexValues(subProperty, propertyCulture, segment, published, availableCultures, contentTypeDictionary); } var rawDataCultures = rawData.Properties.Select(property => property.Culture).Distinct().WhereNotNull().ToArray(); foreach (IndexValue indexValue in indexValues) { indexValue.FieldName = $"{keyPrefix}.{indexValue.FieldName}"; if (indexValue.Culture is null && rawDataCultures.Any()) { foreach (var rawDataCulture in rawDataCultures) { yield return new IndexValue { Culture = rawDataCulture, FieldName = indexValue.FieldName, Values = indexValue.Values }; } } else { indexValue.Culture = rawDataCultures.Any() ? indexValue.Culture : null; yield return indexValue; } } } } } private RawDataItem ToRawData(BlockItemData blockItemData) => ToRawData(blockItemData.ContentTypeKey, blockItemData.Values); private RawDataItem ToRawData(Guid contentTypeKey, IEnumerable values) => new() { ContentTypeKey = contentTypeKey, Properties = values.Select(value => new RawPropertyData { Alias = value.Alias, Culture = value.Culture, Value = value.Value }) }; protected class RawDataItem { public required Guid ContentTypeKey { get; init; } public required IEnumerable Properties { get; init; } } protected class RawPropertyData { public required string Alias { get; init; } public required object? Value { get; init; } public required string? Culture { get; init; } } }