Skip to content

Instantly share code, notes, and snippets.

@karlstal
Last active April 5, 2024 13:08
Show Gist options
  • Save karlstal/73f02eff50bcf017809c07f2f780e77b to your computer and use it in GitHub Desktop.
Save karlstal/73f02eff50bcf017809c07f2f780e77b to your computer and use it in GitHub Desktop.
1. Shows ContentArea items with the same access rights as parent content. 2. Limits expansion depth. 3. Guards against stack overflow
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.Find.Cms;
using EPiServer.Security;
using EPiServer.ServiceLocation;
namespace CdApiHacks.ContentApiExtensions;
public static class ContentApiHelpers
{
public static ContentModelReference ContentModelReference(this IContent content)
{
return new ContentModelReference
{
GuidValue = content.ContentGuid,
Id = content.ContentLink.ID,
WorkId = content.ContentLink.WorkID,
ProviderName = content.ContentLink.ProviderName
};
}
public static ContentReference GetContentReference(this ContentModelReference contentModelReference)
{
return new ContentReference(
contentModelReference.Id.Value,
contentModelReference.WorkId.Value,
contentModelReference.ProviderName);
}
public static bool AllParentRolesInItem(IEnumerable<string> roles, IEnumerable<string> parentRoles)
{
var nCommon = roles.Intersect(parentRoles).Count();
return nCommon != 0 && nCommon == parentRoles.Count();
}
public static IEnumerable<string> ParentRoles(this PropertyData propertyData)
{
var ownerRef = propertyData.Parent.OwnerLink.ToReferenceWithoutVersion();
var contentLoader = ServiceLocator.Current.GetService<IContentLoader>();
var ownerContent = contentLoader.Get<IContent>(ownerRef);
if (ownerContent is IContentSecurable securable)
{
return securable.RolesWithReadAccess();
}
return new List<string>();
}
}
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Internal;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.Core.Internal;
using EPiServer.SpecializedProperties;
using Newtonsoft.Json;
namespace CdApiHacks.ContentApiExtensions;
// This class is identical to the original BlockPropertyConverter,
// except that it uses ExtendedConverterContext instead of ConverterContext
public class CustomBlockPropertyConverter : IPropertyConverter
{
private readonly IContentTypeRepository _contentTypeRepository;
private readonly ReflectionService _reflectionService;
private readonly IPropertyConverterResolver _propertyConverterResolver;
private readonly IPropertyDefinitionTypeRepository _propertyDefinitionTypeRepository;
public CustomBlockPropertyConverter(IContentTypeRepository contentTypeRepository, ReflectionService reflectionService, IPropertyConverterResolver propertyConverterResolver, IPropertyDefinitionTypeRepository propertyDefinitionTypeRepository)
{
_contentTypeRepository = contentTypeRepository;
_reflectionService = reflectionService;
_propertyConverterResolver = propertyConverterResolver;
_propertyDefinitionTypeRepository = propertyDefinitionTypeRepository;
}
public IPropertyModel Convert(PropertyData propertyData, ConverterContext converterContext)
{
var propertyBlock = (IPropertyBlock)propertyData;
var blockPropertyDefinitionType = (BlockPropertyDefinitionType)_propertyDefinitionTypeRepository.Load(propertyBlock.BlockPropertyDefinitionTypeID);
var blockType = _contentTypeRepository.Load(blockPropertyDefinitionType.BlockType.GUID);
return new BlockPropertyModel
{
ExcludePersonalizedContent = converterContext.ExcludePersonalizedContent,
Name = propertyData.Name,
PropertyDataProperty = propertyData as PropertyBlock,
PropertyItemType = blockType.Name,
Properties = ExtractProperties(propertyBlock.Block, converterContext)
};
}
internal IDictionary<string, object> ExtractProperties(BlockData blockData, ConverterContext converterContext)
{
var blockProperties = new Dictionary<string, object>();
var contentType = _contentTypeRepository.Load(blockData.ContentTypeID);
foreach (var property in blockData.Property)
{
if (contentType is object && ShouldPropertyBeIgnored(contentType, property, converterContext))
{
continue;
}
var converter = _propertyConverterResolver.Resolve(property);
if (converter is null)
{
continue;
}
// This is the only change, since we want to keep all data in our custom ConverterContext.
var propertyModel = converter.Convert(property, new ExtendedConverterContext(converterContext.ContentReference, converterContext));
blockProperties.Add(property.Name, propertyModel);
}
return blockProperties;
}
private bool ShouldPropertyBeIgnored(ContentType contentType, PropertyData property, ConverterContext converterContext)
{
if (!converterContext.Options.IncludeEmptyContentProperties && property.IsNull)
{
return true;
}
var contentLink = property.GetContentLink();
if (contentLink != null && converterContext is ExtendedConverterContext extendedContext && extendedContext.ParentExists(contentLink))
{
return true;
}
var propertyAttributes = _reflectionService.GetAttributes(contentType, property);
return propertyAttributes != null && propertyAttributes.OfType<JsonIgnoreAttribute>().Any();
}
}
using System.Security.Principal;
using EPiServer.ContentApi.Core.Internal;
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Internal;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.ContentApi.Core.Serialization.Models.Internal;
using EPiServer.Find;
using EPiServer.Find.Cms;
using EPiServer.Security;
namespace CdApiHacks.ContentApiExtensions;
public class CustomContentExpander : IContentExpander
{
private readonly ContentLoaderService _contentLoaderService;
private readonly ContentConvertingService _contentConvertingService;
private readonly IContentAccessEvaluator _accessEvaluator;
private readonly IPrincipalAccessor _principalAccessor;
private static readonly IPrincipal AnonymousPrincipal = new GenericPrincipal(new GenericIdentity("Anonymous"), []);
public CustomContentExpander(
ContentLoaderService contentLoaderService,
ContentConvertingService contentConvertingService,
IContentAccessEvaluator accessEvaluator,
IPrincipalAccessor principalAccessor)
{
_contentLoaderService = contentLoaderService;
_contentConvertingService = contentConvertingService;
_accessEvaluator = accessEvaluator;
_principalAccessor = principalAccessor;
}
public ContentApiModel Expand(ContentModelReference contentModelReference, ConverterContext context)
{
var content = contentModelReference is not null ?
_contentLoaderService.Get(contentModelReference.GetContentReference(), context.Language?.Name)
: null;
if (content is null)
{
return null;
}
var principal = context.ExcludePersonalizedContent
? GetAnonymousPrincipal()
: _principalAccessor.Principal;
var newContext = new ExtendedConverterContext(content.ContentLink, context);
if (HasParentGroups(content, newContext) || _accessEvaluator.HasAccess(content, principal, AccessLevel.Read))
{
return _contentConvertingService.ConvertToContentApiModel(content, newContext);
}
return null;
}
private bool HasParentGroups(IContent content, ExtendedConverterContext context)
{
if (content is not IContentSecurable securable)
{
return false;
}
var roles = securable.RolesWithReadAccess();
return ContentApiHelpers.AllParentRolesInItem(roles, context.ParentRoles);
}
public static IPrincipal GetAnonymousPrincipal() => VirtualRolePrincipal.CreateWrapper(AnonymousPrincipal);
}
using System.Globalization;
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.ContentApi.Core.Serialization.Models.Internal;
using EPiServer.ServiceLocation;
using EPiServer.SpecializedProperties;
namespace CdApiHacks.ContentApiExtensions;
public class CustomContentReferenceListPropertyModel : ContentReferenceListPropertyModel
{
private readonly IEnumerable<string> _parentRoles;
private readonly IContentExpander _contentExpander;
public CustomContentReferenceListPropertyModel(
PropertyContentReferenceList propertyContentReferenceList, ConverterContext converterContext)
: base(propertyContentReferenceList, converterContext)
{
_parentRoles = propertyContentReferenceList.ParentRoles();
_contentExpander = ServiceLocator.Current.GetInstance<IContentExpander>();
}
protected override IEnumerable<ContentApiModel> ExtractExpandedValue(CultureInfo language)
{
language ??= CultureInfo.InvariantCulture;
var extConverterContext = new ExtendedConverterContext(language, ConverterContext)
{
ParentRoles = _parentRoles
};
if (extConverterContext.IndexingDepth >= extConverterContext.MaxDepth)
{
return new List<ContentApiModel>();
}
extConverterContext.IndexingDepth += 1;
return Value.Select(v => _contentExpander.Expand(v, extConverterContext)).Where(m => m is not null).ToArray();
}
}
using System.Globalization;
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.ContentApi.Core.Serialization.Models.Internal;
using EPiServer.ServiceLocation;
namespace CdApiHacks.ContentApiExtensions;
public class CustomContentReferencePropertyModel : ContentReferencePropertyModel
{
private readonly IEnumerable<string> _parentRoles;
private readonly IContentExpander _contentExpander;
public CustomContentReferencePropertyModel(PropertyContentReference propertyContentReference, ConverterContext converterContext)
: base(propertyContentReference, converterContext)
{
_parentRoles = propertyContentReference.ParentRoles();
_contentExpander = ServiceLocator.Current.GetInstance<IContentExpander>();
}
protected override ContentApiModel ExtractExpandedValue(CultureInfo language)
{
language ??= CultureInfo.InvariantCulture;
var extConverterContext = new ExtendedConverterContext(language, ConverterContext)
{
ParentRoles = _parentRoles
};
if (extConverterContext.IndexingDepth >= extConverterContext.MaxDepth)
{
return null;
}
extConverterContext.IndexingDepth += 1;
return _contentExpander.Expand(Value, extConverterContext);
}
}
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.ContentApi.Core.Serialization.Models.Internal;
using EPiServer.ServiceLocation;
using EPiServer.SpecializedProperties;
namespace CdApiHacks.ContentApiExtensions;
[ServiceConfiguration(typeof(IPropertyConverter), Lifecycle = ServiceInstanceScope.Singleton)]
public class CustomPropertyConverter : IPropertyConverter
{
public IPropertyModel Convert(PropertyData propertyData, ConverterContext converterContext)
{
switch (propertyData)
{
case PropertyContentArea propertyContentArea:
var contentAreaPropertyModel = new ExpandedContentAreaPropertyModel(propertyContentArea, converterContext,
ServiceLocator.Current.GetInstance<IContentExpander>());
contentAreaPropertyModel.Expand(converterContext.Language);
return contentAreaPropertyModel;
case PropertyContentReferenceList propertyContentReferenceList:
var contentReferenceListPropertyModel = new CustomContentReferenceListPropertyModel(propertyContentReferenceList, converterContext);
contentReferenceListPropertyModel.Expand(converterContext.Language);
return contentReferenceListPropertyModel;
case PropertyContentReference propertyContentReference:
var contentReferencePropertyModel = new CustomContentReferencePropertyModel(propertyContentReference, converterContext);
contentReferencePropertyModel.Expand(converterContext.Language);
return contentReferencePropertyModel;
default:
throw new InvalidOperationException();
}
}
}
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Internal;
using EPiServer.ServiceLocation;
using EPiServer.SpecializedProperties;
namespace CdApiHacks.ContentApiExtensions;
[ServiceConfiguration(typeof(IPropertyConverterProvider), Lifecycle = ServiceInstanceScope.Singleton)]
public class CustomPropertyConverterProvider : IPropertyConverterProvider
{
private readonly IContentTypeRepository _contentTypeRepository;
private readonly ReflectionService _reflectionService;
private readonly IPropertyDefinitionTypeRepository _propertyDefinitionTypeRepository;
private IPropertyConverterResolver _propertyConverterResolver;
private CustomBlockPropertyConverter _blockPropertyModelConverter;
public int SortOrder => 200;
public CustomPropertyConverterProvider(
IContentTypeRepository contentTypeRepository,
IPropertyDefinitionTypeRepository propertyDefinitionTypeRepository,
ReflectionService reflectionService)
{
_contentTypeRepository = contentTypeRepository;
_propertyDefinitionTypeRepository = propertyDefinitionTypeRepository;
_reflectionService = reflectionService;
}
public IPropertyConverter Resolve(PropertyData propertyData)
{
if (propertyData is PropertyContentArea || propertyData is PropertyContentReferenceList ||
propertyData is PropertyContentReference)
{
return new CustomPropertyConverter();
}
if (propertyData is PropertyBlock)
{
if (_propertyConverterResolver is null)
{
_propertyConverterResolver = ServiceLocator.Current.GetInstance<IPropertyConverterResolver>();
}
return _blockPropertyModelConverter ??= new CustomBlockPropertyConverter(_contentTypeRepository, _reflectionService, _propertyConverterResolver, _propertyDefinitionTypeRepository);
}
return null!;
}
}
using EPiServer.ContentApi.Core.Internal;
using EPiServer.ContentApi.Core.Serialization;
using EPiServer.ContentApi.Core.Serialization.Models;
using EPiServer.ContentApi.Core.Serialization.Models.Internal;
using EPiServer.Find.Cms;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.SpecializedProperties;
using System.Globalization;
namespace CdApiHacks.ContentApiExtensions;
/// This class is needed for expanding Content Areas and nested content inside of them in Content Delivery API
/// Code originally from Optimizely support
/// </summary>
public class ExpandedContentAreaPropertyModel : ContentAreaPropertyModel
{
private readonly IEnumerable<string> _parentRoles;
private readonly IContentExpander _contentExpander;
public ExpandedContentAreaPropertyModel(PropertyContentArea propertyContentArea,
ConverterContext converterContext, IContentExpander contentExpander) : base(propertyContentArea, converterContext)
{
_parentRoles = propertyContentArea.ParentRoles();
_contentExpander = contentExpander;
Value = GetValue(propertyContentArea, converterContext.Language?.Name);
}
private IEnumerable<ContentAreaItemModel> GetValue(PropertyContentArea propertyContentArea, string language)
{
var values = Value?.ToList() ?? new List<ContentAreaItemModel>();
if (propertyContentArea.Value is not ContentArea contentArea)
{
return values;
}
var _contentLoaderService = ServiceLocator.Current.GetService<ContentLoaderService>();
foreach (var item in contentArea.Items)
{
var content = _contentLoaderService.Get(item.ContentLink, language);
if (content is not IContentSecurable securable)
{
continue;
}
var roles = securable.RolesWithReadAccess();
if (ContentApiHelpers.AllParentRolesInItem(roles, _parentRoles)
&& !values.Select(x => x.ContentLink.GuidValue).Where(x => x.Equals(content.ContentGuid)).Any())
{
values.Add(new ContentAreaItemModel
{
ContentLink = content.ContentModelReference()
});
}
}
return values;
}
protected override IEnumerable<ContentApiModel> ExtractExpandedValue(CultureInfo language)
{
var expandedValue = new List<ContentApiModel>();
if (ConverterContext is ExtendedConverterContext ec && ec.IndexingDepth >= ec.MaxDepth)
{
return new List<ContentApiModel>();
}
foreach (var item in Value)
{
var contentReference = new ContentReference(item.ContentLink.Id!.Value, item.ContentLink.WorkId!.Value);
if (ConverterContext is ExtendedConverterContext extendedConverterContext && extendedConverterContext.ParentExists(contentReference))
{
continue;
}
var newContext = new ExtendedConverterContext(contentReference, ConverterContext, _parentRoles);
newContext.IndexingDepth += 1;
var expandedModel = _contentExpander.Expand(item.ContentLink, newContext);
expandedValue.Add(expandedModel);
}
return expandedValue;
}
}
using System.Globalization;
using EPiServer.ContentApi.Core.Serialization;
namespace CdApiHacks.ContentApiExtensions;
public class ExtendedConverterContext : ConverterContext
{
public int MaxDepth {get; set;} = 3;
public IEnumerable<string> ParentRoles {get; set;}
public List<ContentReference> ParentReferences {get; set;}
public int IndexingDepth {get; set;}
public ExtendedConverterContext(ContentReference contentReference, ConverterContext oldContext, IEnumerable<string> parentRoles)
: this(contentReference, oldContext)
{
ParentRoles = parentRoles;
}
public ExtendedConverterContext(ContentReference contentReference, ConverterContext oldContext)
: this(contentReference, oldContext, oldContext.Language)
{}
public ExtendedConverterContext(CultureInfo language, ConverterContext oldContext)
: this(oldContext.ContentReference, oldContext, language)
{}
private ExtendedConverterContext(ContentReference contentReference, ConverterContext oldContext, CultureInfo language)
: base(
contentReference,
language,
oldContext.Options,
oldContext.ContextMode,
string.Join(",", oldContext.SelectedProperties), // might need a null check here
string.Join(",", oldContext.ExpandedProperties), // might need a null check here
false // By using this class we never exclude personalized content.
)
{
if (oldContext is ExtendedConverterContext extendedConverterContext)
{
ParentReferences = extendedConverterContext.ParentReferences.ToList();
if (!ParentExists(contentReference))
{
ParentReferences.Add(contentReference);
}
ParentRoles = extendedConverterContext.ParentRoles;
IndexingDepth = extendedConverterContext.IndexingDepth;
}
else
{
ParentRoles = new List<string>();
IndexingDepth = 0;
ParentReferences = new List<ContentReference> {contentReference};
}
}
public bool ParentExists(ContentReference reference)
{
return ParentReferences.Where(x => x.CompareToIgnoreWorkID(reference)).Any();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment