using System; using System.Collections.Generic; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent { /// /// Provides a default implementation for . /// public class PublishedValueFallback : IPublishedValueFallback { private readonly ILocalizationService _localizationService; private readonly IVariationContextAccessor _variationContextAccessor; /// /// Initializes a new instance of the class. /// public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor) { _localizationService = serviceContext.LocalizationService; _variationContextAccessor = variationContextAccessor; } /// public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) { return TryGetValue(property, culture, segment, fallback, defaultValue, out value); } /// public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) { _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); foreach (var f in fallback) { switch (f) { case Fallback.None: continue; case Fallback.DefaultValue: value = defaultValue; return true; case Fallback.Language: if (TryGetValueWithLanguageFallback(property, culture, segment, out value)) return true; break; default: throw NotSupportedFallbackMethod(f, "property"); } } value = default; return false; } /// public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) { return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); } /// public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) { var propertyType = content.ContentType.GetPropertyType(alias); if (propertyType == null) { value = default; return false; } _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); foreach (var f in fallback) { switch (f) { case Fallback.None: continue; case Fallback.DefaultValue: value = defaultValue; return true; case Fallback.Language: if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) return true; break; default: throw NotSupportedFallbackMethod(f, "element"); } } value = default; return false; } /// public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) { return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty); } /// public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) { noValueProperty = default; var propertyType = content.ContentType.GetPropertyType(alias); if (propertyType != null) { _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); noValueProperty = content.GetProperty(alias); } // note: we don't support "recurse & language" which would walk up the tree, // looking at languages at each level - should someone need it... they'll have // to implement it. foreach (var f in fallback) { switch (f) { case Fallback.None: continue; case Fallback.DefaultValue: value = defaultValue; return true; case Fallback.Language: if (propertyType == null) continue; if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) return true; break; case Fallback.Ancestors: if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) return true; break; default: throw NotSupportedFallbackMethod(f, "content"); } } value = default; return false; } private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) { return new NotSupportedException($"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); } // tries to get a value, recursing the tree // because we recurse, content may not even have the a property with the specified alias (but only some ancestor) // in case no value was found, noValueProperty contains the first property that was found (which does not have a value) private bool TryGetValueWithAncestorsFallback(IPublishedContent? content, string alias, string? culture, string? segment, out T? value, ref IPublishedProperty? noValueProperty) { IPublishedProperty? property; // if we are here, content's property has no value do { content = content?.Parent; var propertyType = content?.ContentType.GetPropertyType(alias); if (propertyType != null && content is not null) { culture = null; segment = null; _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); } property = content?.GetProperty(alias); if (property != null && noValueProperty == null) { noValueProperty = property; } } while (content != null && (property == null || property.HasValue(culture, segment) == false)); // if we found a content with the property having a value, return that property value if (property != null && property.HasValue(culture, segment)) { value = property.Value(this, culture, segment); return true; } value = default; return false; } // tries to get a value, falling back onto other languages private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) { value = default; if (culture.IsNullOrWhiteSpace()) return false; var visited = new HashSet(); var language = culture is not null ? _localizationService.GetLanguageByIsoCode(culture) : null; if (language == null) return false; while (true) { if (language.FallbackLanguageId == null) return false; var language2Id = language.FallbackLanguageId.Value; if (visited.Contains(language2Id)) return false; visited.Add(language2Id); var language2 = _localizationService.GetLanguageById(language2Id); if (language2 == null) return false; var culture2 = language2.IsoCode; if (property.HasValue(culture2, segment)) { value = property.Value(this, culture2, segment); return true; } language = language2; } } // tries to get a value, falling back onto other languages private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string? culture, string? segment, out T? value) { value = default; if (culture.IsNullOrWhiteSpace()) return false; var visited = new HashSet(); var language = culture is not null ? _localizationService.GetLanguageByIsoCode(culture) : null; if (language == null) return false; while (true) { if (language.FallbackLanguageId == null) return false; var language2Id = language.FallbackLanguageId.Value; if (visited.Contains(language2Id)) return false; visited.Add(language2Id); var language2 = _localizationService.GetLanguageById(language2Id); if (language2 == null) return false; var culture2 = language2.IsoCode; if (content.HasValue(alias, culture2, segment)) { value = content.Value(this, alias, culture2, segment); return true; } language = language2; } } // tries to get a value, falling back onto other languages private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string? culture, string? segment, out T? value) { value = default; if (culture.IsNullOrWhiteSpace()) return false; var visited = new HashSet(); // TODO: _localizationService.GetXxx() is expensive, it deep clones objects // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone var language = culture is not null ? _localizationService.GetLanguageByIsoCode(culture) : null; if (language == null) return false; while (true) { if (language.FallbackLanguageId == null) return false; var language2Id = language.FallbackLanguageId.Value; if (visited.Contains(language2Id)) return false; visited.Add(language2Id); var language2 = _localizationService.GetLanguageById(language2Id); if (language2 == null) return false; var culture2 = language2.IsoCode; if (content.HasValue(alias, culture2, segment)) { value = content.Value(this, alias, culture2, segment); return true; } language = language2; } } } }