Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a98a47db0 | ||
|
|
18a9da080a | ||
|
|
6104efdd47 | ||
|
|
7d400e7742 | ||
|
|
8295f833f8 | ||
|
|
8d6645b0ef | ||
|
|
a0cb9b2826 | ||
|
|
b2ff910a92 | ||
|
|
b70d2b482d | ||
|
|
ac22eef92f | ||
|
|
24ef23e133 | ||
|
|
c8fb306e67 |
@@ -46,8 +46,8 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine" Version="3.8.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.8.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
@@ -87,7 +87,7 @@
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
|
||||
@@ -132,6 +132,30 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects if any persisted property types have been removed by comparing old and new collections,
|
||||
/// and sets <see cref="HasPropertyTypeBeenRemoved"/> accordingly.
|
||||
/// </summary>
|
||||
private void DetectPropertyTypeRemovals(IEnumerable<IPropertyType> oldPropertyTypes, IEnumerable<IPropertyType> newPropertyTypes)
|
||||
{
|
||||
if (HasPropertyTypeBeenRemoved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
|
||||
if (oldIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newIds = new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
|
||||
if (oldIds.Any(id => !newIds.Contains(id)))
|
||||
{
|
||||
HasPropertyTypeBeenRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyTypes that are not part of a PropertyGroup
|
||||
/// </summary>
|
||||
@@ -273,6 +297,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
{
|
||||
if (PropertyTypeCollection != null)
|
||||
{
|
||||
DetectPropertyTypeRemovals(PropertyTypeCollection, value);
|
||||
PropertyTypeCollection.ClearCollectionChangedEvents();
|
||||
}
|
||||
|
||||
@@ -443,21 +468,21 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
/// Please note that resetting the dirty properties could potentially
|
||||
/// obstruct the saving of a new or updated entity.
|
||||
/// </remarks>
|
||||
public override void ResetDirtyProperties()
|
||||
public override void ResetDirtyProperties(bool rememberDirty)
|
||||
{
|
||||
base.ResetDirtyProperties();
|
||||
base.ResetDirtyProperties(rememberDirty);
|
||||
|
||||
// loop through each property group to reset the property types
|
||||
var propertiesReset = new List<int>();
|
||||
|
||||
foreach (PropertyGroup propertyGroup in PropertyGroups)
|
||||
{
|
||||
propertyGroup.ResetDirtyProperties();
|
||||
propertyGroup.ResetDirtyProperties(rememberDirty);
|
||||
if (propertyGroup.PropertyTypes is not null)
|
||||
{
|
||||
foreach (IPropertyType propertyType in propertyGroup.PropertyTypes)
|
||||
{
|
||||
propertyType.ResetDirtyProperties();
|
||||
propertyType.ResetDirtyProperties(rememberDirty);
|
||||
propertiesReset.Add(propertyType.Id);
|
||||
}
|
||||
}
|
||||
@@ -467,7 +492,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
|
||||
// but don't re-reset ones we've already done.
|
||||
foreach (IPropertyType propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false))
|
||||
{
|
||||
propertyType.ResetDirtyProperties();
|
||||
propertyType.ResetDirtyProperties(rememberDirty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
|
||||
{
|
||||
private List<IContentTypeComposition> _contentTypeComposition = new();
|
||||
private List<int> _removedContentTypeKeyTracker = new();
|
||||
private bool _hasCompositionBeenRemoved;
|
||||
|
||||
protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId)
|
||||
: base(shortStringHelper, parentId)
|
||||
@@ -104,6 +105,24 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A boolean flag indicating if a composition has been removed from this instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is currently (specifically) used in order to know that we need to refresh the content cache which
|
||||
/// needs to occur when a composition has been removed from a content type
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
internal bool HasCompositionTypeBeenRemoved
|
||||
{
|
||||
get => _hasCompositionBeenRemoved;
|
||||
private set
|
||||
{
|
||||
_hasCompositionBeenRemoved = value;
|
||||
OnPropertyChanged(nameof(HasCompositionTypeBeenRemoved));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IPropertyType> GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes();
|
||||
|
||||
@@ -179,6 +198,8 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
|
||||
_removedContentTypeKeyTracker.AddRange(compositionIdsToRemove);
|
||||
}
|
||||
|
||||
HasCompositionTypeBeenRemoved = true;
|
||||
|
||||
OnPropertyChanged(nameof(ContentTypeComposition));
|
||||
|
||||
return _contentTypeComposition.Remove(contentTypeComposition);
|
||||
|
||||
@@ -20,6 +20,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
|
||||
private string _alias;
|
||||
private string? _name;
|
||||
private bool _hasPropertyTypeBeenRemoved;
|
||||
private PropertyTypeCollection? _propertyTypes;
|
||||
private int _sortOrder;
|
||||
|
||||
@@ -94,6 +95,20 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A boolean flag indicating if a property type has been removed from this group.
|
||||
/// </summary>
|
||||
[IgnoreDataMember]
|
||||
internal bool HasPropertyTypeBeenRemoved
|
||||
{
|
||||
get => _hasPropertyTypeBeenRemoved;
|
||||
private set
|
||||
{
|
||||
_hasPropertyTypeBeenRemoved = value;
|
||||
OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of property types for the group.
|
||||
/// </summary>
|
||||
@@ -112,6 +127,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
{
|
||||
if (_propertyTypes != null)
|
||||
{
|
||||
DetectPropertyTypeRemovals(_propertyTypes, value);
|
||||
_propertyTypes.ClearCollectionChangedEvents();
|
||||
}
|
||||
|
||||
@@ -155,4 +171,27 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
|
||||
|
||||
private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
||||
OnPropertyChanged(nameof(PropertyTypes));
|
||||
|
||||
private void DetectPropertyTypeRemovals(PropertyTypeCollection oldPropertyTypes, PropertyTypeCollection? newPropertyTypes)
|
||||
{
|
||||
if (HasPropertyTypeBeenRemoved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
|
||||
if (oldIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newIds = newPropertyTypes != null
|
||||
? new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0))
|
||||
: new HashSet<int>();
|
||||
|
||||
if (oldIds.Any(id => !newIds.Contains(id)))
|
||||
{
|
||||
HasPropertyTypeBeenRemoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,20 @@ public static class ContentTypeChangeExtensions
|
||||
|
||||
public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
|
||||
(change & types) == ContentTypeChangeTypes.None;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the change has structural change impact.
|
||||
/// </summary>
|
||||
/// <param name="change">The change to check.</param>
|
||||
/// <returns><c>true</c> if the change has structural impact; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsStructuralChange(this ContentTypeChangeTypes change) =>
|
||||
change.HasType(ContentTypeChangeTypes.RefreshMain);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the change has non-structural change impact.
|
||||
/// </summary>
|
||||
/// <param name="change">The change to check.</param>
|
||||
/// <returns><c>true</c> if the change has non-structural impact; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsNonStructuralChange(this ContentTypeChangeTypes change) =>
|
||||
change.HasType(ContentTypeChangeTypes.RefreshOther) && !change.HasType(ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,29 @@ public enum ContentTypeChangeTypes : byte
|
||||
Create = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Content type changes impact only the Content type being saved
|
||||
/// Content type changes directly impact existing content of this content type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These changes are "destructive" of nature. They include:
|
||||
/// - Changing the content type alias.
|
||||
/// - Removing a property type or a composition.
|
||||
/// - Changing the alias of a property type (this effectively corresponds to removing a property type).
|
||||
/// - Changing variance, either at property or content type level.
|
||||
/// </remarks>
|
||||
RefreshMain = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Content type changes impacts the content type being saved and others used that are composed of it
|
||||
/// Content type changes that do not directly impact existing content of this content type.
|
||||
/// </summary>
|
||||
RefreshOther = 4, // changed, other change
|
||||
/// <remarks>
|
||||
/// These changes are "constructive" of nature, and include all changes not included in
|
||||
/// <see cref="RefreshMain"/> - for example:
|
||||
/// - Adding a property type or a composition.
|
||||
/// - Rearranging property types or groups.
|
||||
/// - Changes to name, description, icon etc.
|
||||
/// - Changes to other content type settings, i.e. allowed child types and version cleanup.
|
||||
/// </remarks>
|
||||
RefreshOther = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Content type was removed
|
||||
|
||||
@@ -226,7 +226,10 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
|
||||
});
|
||||
|
||||
// removed properties?
|
||||
var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
|
||||
// check both the content type level flag (set by RemovePropertyType) and
|
||||
// individual property group flags (set when PropertyTypes collection is replaced, e.g. by the mapper)
|
||||
var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved")
|
||||
|| contentType.PropertyGroups.Any(g => g.WasPropertyDirty("HasPropertyTypeBeenRemoved"));
|
||||
|
||||
// removed compositions?
|
||||
var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
|
||||
|
||||
@@ -52,16 +52,16 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)>();
|
||||
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds)>();
|
||||
|
||||
foreach (ContentTypeCacheRefresher.JsonPayload payload in (ContentTypeCacheRefresher.JsonPayload[])args
|
||||
.MessageObject)
|
||||
{
|
||||
if (!changedIds.TryGetValue(
|
||||
payload.ItemType,
|
||||
out (List<int> removedIds, List<int> refreshedIds, List<int> otherIds) idLists))
|
||||
out (List<int> removedIds, List<int> refreshedIds) idLists))
|
||||
{
|
||||
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>(), otherIds: new List<int>());
|
||||
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>());
|
||||
changedIds.Add(payload.ItemType, idLists);
|
||||
}
|
||||
|
||||
@@ -73,28 +73,24 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
|
||||
{
|
||||
idLists.refreshedIds.Add(payload.Id);
|
||||
}
|
||||
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther))
|
||||
{
|
||||
idLists.otherIds.Add(payload.Id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)> ci in
|
||||
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds)> ci in
|
||||
changedIds)
|
||||
{
|
||||
if (ci.Value.refreshedIds.Count > 0 || ci.Value.otherIds.Count > 0)
|
||||
if (ci.Value.refreshedIds.Count > 0)
|
||||
{
|
||||
switch (ci.Key)
|
||||
{
|
||||
case var itemType when itemType == typeof(IContentType).Name:
|
||||
RefreshContentOfContentTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
|
||||
RefreshContentOfContentTypes(ci.Value.refreshedIds.Distinct()
|
||||
.ToArray());
|
||||
break;
|
||||
case var itemType when itemType == typeof(IMediaType).Name:
|
||||
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct().ToArray());
|
||||
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Distinct().ToArray());
|
||||
break;
|
||||
case var itemType when itemType == typeof(IMemberType).Name:
|
||||
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
|
||||
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Distinct()
|
||||
.ToArray());
|
||||
break;
|
||||
}
|
||||
@@ -154,6 +150,10 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
|
||||
// track which Ids have their paths are published
|
||||
var publishChecked = new Dictionary<int, bool>();
|
||||
|
||||
while (page * pageSize < total)
|
||||
{
|
||||
IEnumerable<IContent> contentToRefresh = _contentService.GetPagedOfTypes(
|
||||
@@ -165,20 +165,20 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
|
||||
// order by shallowest to deepest, this allows us to check it's published state without checking every item
|
||||
Ordering.By("Path"));
|
||||
|
||||
// track which Ids have their paths are published
|
||||
var publishChecked = new Dictionary<int, bool>();
|
||||
|
||||
foreach (IContent c in contentToRefresh)
|
||||
{
|
||||
var isPublished = false;
|
||||
if (c.Published)
|
||||
{
|
||||
if (!publishChecked.TryGetValue(c.ParentId, out isPublished))
|
||||
if (publishChecked.TryGetValue(c.ParentId, out isPublished) is false)
|
||||
{
|
||||
// nothing by parent id, so query the service and cache the result for the next child to check against
|
||||
isPublished = _contentService.IsPathPublished(c);
|
||||
publishChecked[c.Id] = isPublished;
|
||||
|
||||
// the parent *must* be published if the entire path is published
|
||||
publishChecked[c.ParentId] = isPublished;
|
||||
}
|
||||
|
||||
publishChecked[c.Id] = isPublished;
|
||||
}
|
||||
|
||||
_umbracoIndexingHandler.ReIndexForContent(c, isPublished);
|
||||
|
||||
@@ -53,9 +53,7 @@ public class PublishedSnapshotServiceEventHandler :
|
||||
|
||||
public void Handle(ContentTypeRefreshedNotification notification)
|
||||
{
|
||||
const ContentTypeChangeTypes types // only for those that have been refreshed
|
||||
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther;
|
||||
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id)
|
||||
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.IsStructuralChange()).Select(x => x.Item.Id)
|
||||
.ToArray();
|
||||
if (contentTypeIds.Any())
|
||||
{
|
||||
|
||||
@@ -51,8 +51,8 @@ public class UmbLoginStatusController : SurfaceController
|
||||
|
||||
TempData["LogoutSuccess"] = true;
|
||||
|
||||
// If there is a specified path to redirect to then use it.
|
||||
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
|
||||
// If there is a specified path to redirect to and it is validated as a local URL, then use it.
|
||||
if (model.RedirectUrl.IsNullOrWhiteSpace() is false && Url.IsLocalUrl(model.RedirectUrl!))
|
||||
{
|
||||
return Redirect(model.RedirectUrl!);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ public class UmbProfileController : SurfaceController
|
||||
|
||||
TempData["FormSuccess"] = true;
|
||||
|
||||
// If there is a specified path to redirect to then use it.
|
||||
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
|
||||
// If there is a specified path to redirect to and it is validated as a local URL, then use it.
|
||||
if (model.RedirectUrl.IsNullOrWhiteSpace() is false && Url.IsLocalUrl(model.RedirectUrl!))
|
||||
{
|
||||
return Redirect(model.RedirectUrl!);
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ public class UmbRegisterController : SurfaceController
|
||||
{
|
||||
TempData["FormSuccess"] = true;
|
||||
|
||||
// If there is a specified path to redirect to then use it.
|
||||
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
|
||||
// If there is a specified path to redirect to and it is validated as a local URL, then use it.
|
||||
if (model.RedirectUrl.IsNullOrWhiteSpace() is false && Url.IsLocalUrl(model.RedirectUrl!))
|
||||
{
|
||||
return Redirect(model.RedirectUrl!);
|
||||
}
|
||||
|
||||
+358
@@ -5,12 +5,16 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Tests.Common.Attributes;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
@@ -35,6 +39,10 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
|
||||
{
|
||||
builder.AddNotificationHandler<ContentMovedToRecycleBinNotification, ContentNotificationHandler>();
|
||||
builder.AddNotificationHandler<ContentTypeDeletedNotification, ContentTypeNotificationHandler>();
|
||||
|
||||
builder.AddNotificationHandler<ContentTypeChangedNotification, ContentTypeChangedDistributedCacheNotificationHandler>();
|
||||
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, ContentTypeCacheRefreshedNotificationHandler>();
|
||||
builder.Services.AddUnique<IServerMessenger, ContentEventsTests.LocalServerMessenger>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -2026,6 +2034,328 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
|
||||
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Adding_ContentType_Composition_Yields_RefreshOther()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType component = CreateComponent();
|
||||
cts.Save(component);
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
Assert.IsTrue(site.AddContentType(component));
|
||||
Assert.IsTrue(site.ContentTypeCompositionExists(component.Alias));
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshOther when adding a compostion
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Adding_PropertyType_Yields_RefreshOther()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
var propertyType =
|
||||
new PropertyType(ShortStringHelper, Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext,
|
||||
"title")
|
||||
{
|
||||
Name = "Title",
|
||||
Description = string.Empty,
|
||||
Mandatory = false,
|
||||
SortOrder = 1,
|
||||
DataTypeId = -88
|
||||
};
|
||||
Assert.IsTrue(site.AddPropertyType(propertyType));
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshOther when adding a property
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Removing_ContentType_Composition_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType component = CreateComponent();
|
||||
cts.Save(component);
|
||||
IContentType site = CreateSite();
|
||||
Assert.IsTrue(site.AddContentType(component));
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
Assert.IsTrue(site.ContentTypeCompositionExists(component.Alias));
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
Assert.IsTrue(site.RemoveContentType(component.Alias));
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when removing a composition
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Removing_PropertyType_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.RemovePropertyType(site.PropertyTypes.First().Alias);
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when removing a property
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Removing_PropertyType_By_Replacing_Collection_Yields_RefreshMain()
|
||||
{
|
||||
// This test simulates how the backoffice mapper removes a property: rather than calling
|
||||
// RemovePropertyType(), it rebuilds the property collection without the removed property
|
||||
// and assigns it to the group. This is the code path in ContentTypeMapDefinition.MapSaveToTypeBase().
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
// Simulate the mapper path: replace the group's PropertyTypes with an empty collection
|
||||
// (as if the property was removed via the backoffice UI)
|
||||
PropertyGroup group = site.PropertyGroups.First();
|
||||
group.PropertyTypes = new PropertyTypeCollection(true);
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when removing a property, regardless of how it was removed
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Removing_PropertyTypeGroup_Yields_RefreshOther()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.RemovePropertyGroup(site.PropertyTypes.First().Alias);
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; removing a group does not cause the contained properties to be removed, so expect RefreshOther
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_PropertyType_Alias_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.PropertyTypes.First().Alias += "_updated";
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when changing the alias of a property (it corresponds to removing the property)
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_ContentType_Alias_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.Alias += "_updated";
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when changing the alias of a content type
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_PropertyType_Variance_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
site.Variations = ContentVariation.Culture;
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.PropertyTypes.First().Variations = ContentVariation.Culture;
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when changing the variance of a property
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_ContentType_Variance_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.Variations = ContentVariation.Culture;
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshMain when changing the variance of a content type
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_User_Interface_Settings_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.Name += "_updated";
|
||||
site.Description += "_updated";
|
||||
site.Icon += "_updated";
|
||||
site.PropertyTypes.First().Name += "_updated";
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshOther when making UI changes only (names, icon, description etc.)
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_History_Cleanup_And_Basic_Structure_Settings_Yields_RefreshMain()
|
||||
{
|
||||
var cts = ContentTypeService;
|
||||
|
||||
// Arrange
|
||||
IContentType site = CreateSite();
|
||||
cts.Save(site);
|
||||
|
||||
// re-fetch before acting
|
||||
site = cts.Get(site.Id)!;
|
||||
|
||||
// Act
|
||||
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
|
||||
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
|
||||
=> refreshedPayloads = payloads;
|
||||
|
||||
site.HistoryCleanup = new HistoryCleanup
|
||||
{
|
||||
KeepAllVersionsNewerThanDays = 12,
|
||||
KeepLatestVersionPerDayForDays = 32,
|
||||
PreventCleanup = false,
|
||||
};
|
||||
site.AllowedAsRoot = !site.AllowedAsRoot;
|
||||
site.AllowedContentTypes = [new ContentTypeSort(site.Id, 1)];
|
||||
cts.Save(site);
|
||||
|
||||
// Assert; expect RefreshOther when making UI changes only (names, icon, description etc.)
|
||||
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
|
||||
}
|
||||
|
||||
private ContentType CreateComponent()
|
||||
{
|
||||
var component = new ContentType(ShortStringHelper, -1)
|
||||
@@ -2169,4 +2499,32 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
|
||||
public static Action<ContentTypeDeletedNotification> Deleted { get; set; }
|
||||
public void Handle(ContentTypeDeletedNotification notification) => Deleted?.Invoke(notification);
|
||||
}
|
||||
|
||||
public class ContentTypeCacheRefreshedNotificationHandler : INotificationHandler<ContentTypeCacheRefresherNotification>
|
||||
{
|
||||
public static Action<ContentTypeCacheRefresher.JsonPayload[]>? ContentTypeCacheRefreshed { get; set; }
|
||||
|
||||
public void Handle(ContentTypeCacheRefresherNotification notification)
|
||||
{
|
||||
if (notification.MessageType != MessageType.RefreshByPayload || notification.MessageObject is not ContentTypeCacheRefresher.JsonPayload[] payloads)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
ContentTypeCacheRefreshed?.Invoke(payloads);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertContentTypeRefreshPayload(ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads, int expectedContentTypeId, ContentTypeChangeTypes expectedChangeTypes)
|
||||
{
|
||||
Assert.IsNotNull(refreshedPayloads);
|
||||
Assert.AreEqual(1, refreshedPayloads.Length);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var payload = refreshedPayloads.First();
|
||||
Assert.AreEqual(expectedContentTypeId, payload.Id);
|
||||
Assert.AreEqual(expectedChangeTypes, payload.ChangeTypes);
|
||||
Assert.AreEqual(nameof(IContentType), payload.ItemType);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
|
||||
|
||||
[TestFixture]
|
||||
public class ContentTypeChangeExtensionsTests
|
||||
{
|
||||
[TestCase(ContentTypeChangeTypes.RefreshMain, true)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther, true)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.Create, true)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshOther, false)]
|
||||
[TestCase(ContentTypeChangeTypes.None, false)]
|
||||
[TestCase(ContentTypeChangeTypes.Create, false)]
|
||||
[TestCase(ContentTypeChangeTypes.Remove, false)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove, false)]
|
||||
public void IsStructuralChange(ContentTypeChangeTypes change, bool expected) =>
|
||||
Assert.AreEqual(expected, change.IsStructuralChange());
|
||||
|
||||
[TestCase(ContentTypeChangeTypes.RefreshOther, true)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Create, true)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove, true)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshMain, false)]
|
||||
[TestCase(ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther, false)]
|
||||
[TestCase(ContentTypeChangeTypes.None, false)]
|
||||
[TestCase(ContentTypeChangeTypes.Create, false)]
|
||||
[TestCase(ContentTypeChangeTypes.Remove, false)]
|
||||
public void IsNonStructuralChange(ContentTypeChangeTypes change, bool expected) =>
|
||||
Assert.AreEqual(expected, change.IsNonStructuralChange());
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "13.13.1",
|
||||
"version": "13.15.0-rc",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user