-
Welcome back @Context?.User?.Identity?.Name!
+
Welcome back @Context.User?.GetMemberIdentity()?.Name!
@using (Html.BeginUmbracoForm
("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
{
diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutAreaItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutAreaItem.cs
index 5ee8bc02af19..5ee2c3c7aa74 100644
--- a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutAreaItem.cs
+++ b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutAreaItem.cs
@@ -11,4 +11,10 @@ public BlockGridLayoutAreaItem()
public BlockGridLayoutAreaItem(Guid key)
=> Key = key;
+
+ public bool ContainsContent(Guid key)
+ => Items.Any(item => item.ReferencesContent(key));
+
+ public bool ContainsSetting(Guid key)
+ => Items.Any(item => item.ReferencesSetting(key));
}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs
index ff977acf9836..79ff8e40bcc7 100644
--- a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs
+++ b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs
@@ -38,4 +38,10 @@ public BlockGridLayoutItem(Guid contentKey, Guid settingsKey)
: base(contentKey, settingsKey)
{
}
+
+ public override bool ReferencesContent(Guid key)
+ => ContentKey == key || Areas.Any(area => area.ContainsContent(key));
+
+ public override bool ReferencesSetting(Guid key)
+ => SettingsKey == key || Areas.Any(area => area.ContainsSetting(key));
}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs b/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs
index 4f4ff9e22acc..154de1618c83 100644
--- a/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs
+++ b/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs
@@ -81,4 +81,10 @@ protected BlockLayoutItemBase(Guid contentKey, Guid settingsKey)
SettingsKey = settingsKey;
SettingsUdi = new GuidUdi(Constants.UdiEntityType.Element, settingsKey);
}
+
+ public virtual bool ReferencesContent(Guid key)
+ => ContentKey == key;
+
+ public virtual bool ReferencesSetting(Guid key)
+ => SettingsKey == key;
}
diff --git a/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs
index 3974bfc1a0b2..a67b0983d6d2 100644
--- a/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs
+++ b/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs
@@ -14,4 +14,8 @@ public interface IBlockLayoutItem
public Guid ContentKey { get; set; }
public Guid? SettingsKey { get; set; }
+
+ public bool ReferencesContent(Guid key) => ContentKey == key;
+
+ public bool ReferencesSetting(Guid key) => SettingsKey == key;
}
diff --git a/src/Umbraco.Core/Models/IWebhook.cs b/src/Umbraco.Core/Models/IWebhook.cs
index ab8c6ed05d0a..7e64b566472f 100644
--- a/src/Umbraco.Core/Models/IWebhook.cs
+++ b/src/Umbraco.Core/Models/IWebhook.cs
@@ -1,9 +1,22 @@
-using Umbraco.Cms.Core.Models.Entities;
+using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models;
public interface IWebhook : IEntity
{
+ // TODO (V16): Remove the default implementations from this interface.
+ string? Name
+ {
+ get { return null; }
+ set { }
+ }
+
+ string? Description
+ {
+ get { return null; }
+ set { }
+ }
+
string Url { get; set; }
string[] Events { get; set; }
diff --git a/src/Umbraco.Core/Models/PublishBranchFilter.cs b/src/Umbraco.Core/Models/PublishBranchFilter.cs
new file mode 100644
index 000000000000..e47a07f67739
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishBranchFilter.cs
@@ -0,0 +1,28 @@
+namespace Umbraco.Cms.Core.Models;
+
+///
+/// Describes the options available with publishing a content branch for force publishing.
+///
+[Flags]
+public enum PublishBranchFilter
+{
+ ///
+ /// The default behavior is to publish only the published content that has changed.
+ ///
+ Default = 0,
+
+ ///
+ /// For publishing a branch, publish all changed content, including content that is not published.
+ ///
+ IncludeUnpublished = 1,
+
+ ///
+ /// For publishing a branch, force republishing of all published content, including content that has not changed.
+ ///
+ ForceRepublish = 2,
+
+ ///
+ /// For publishing a branch, publish all content, including content that is not published and content that has not changed.
+ ///
+ All = IncludeUnpublished | ForceRepublish,
+}
diff --git a/src/Umbraco.Core/Models/PublishNotificationSaveOptions.cs b/src/Umbraco.Core/Models/PublishNotificationSaveOptions.cs
new file mode 100644
index 000000000000..f49e7f30d685
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishNotificationSaveOptions.cs
@@ -0,0 +1,28 @@
+namespace Umbraco.Cms.Core.Models;
+
+///
+/// Specifies options for publishing notifcations when saving.
+///
+[Flags]
+public enum PublishNotificationSaveOptions
+{
+ ///
+ /// Do not publish any notifications.
+ ///
+ None = 0,
+
+ ///
+ /// Only publish the saving notification.
+ ///
+ Saving = 1,
+
+ ///
+ /// Only publish the saved notification.
+ ///
+ Saved = 2,
+
+ ///
+ /// Publish all the notifications.
+ ///
+ All = Saving | Saved,
+}
diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs
index 499cee018690..0528b3a6ef4f 100644
--- a/src/Umbraco.Core/Models/Webhook.cs
+++ b/src/Umbraco.Core/Models/Webhook.cs
@@ -1,4 +1,4 @@
-using Umbraco.Cms.Core.Models.Entities;
+using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models;
@@ -24,6 +24,8 @@ private static readonly DelegateEqualityComparer>
(enumerable, translations) => enumerable.UnsortedSequenceEqual(translations),
enumerable => enumerable.GetHashCode());
+ private string? _name;
+ private string? _description;
private string _url;
private string[] _events;
private Guid[] _contentTypeKeys;
@@ -39,6 +41,18 @@ public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, stri
_enabled = enabled ?? false;
}
+ public string? Name
+ {
+ get => _name;
+ set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name));
+ }
+
+ public string? Description
+ {
+ get => _description;
+ set => SetPropertyValueAndDetectChanges(value, ref _description!, nameof(Description));
+ }
+
public string Url
{
get => _url;
diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs
index c719df2b5040..cbb012bf04c6 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs
@@ -1,4 +1,4 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Persistence.Repositories;
@@ -8,6 +8,17 @@ public interface IWebhookLogRepository
Task> GetPagedAsync(int skip, int take);
+ // TODO (V16): Remove the default implementation on this method.
+ async Task> GetPagedAsync(Guid webhookKey, int skip, int take)
+ {
+ // This is very inefficient as the filter/skip/take is in-memory, but it will return the correct data.
+ // As it's only here to avoid a breaking change on the interface that is unlikely to have a custom
+ // implementation, this seems reasonable.
+ PagedModel allLogs = await GetPagedAsync(0, int.MaxValue);
+ var logsForId = allLogs.Items.Where(x => x.WebhookKey == webhookKey).ToList();
+ return new PagedModel(logsForId.Count, logsForId.Skip(skip).Take(take));
+ }
+
Task> GetOlderThanDate(DateTime date);
Task DeleteByIds(int[] ids);
diff --git a/src/Umbraco.Core/PublishedCache/ITagQuery.cs b/src/Umbraco.Core/PublishedCache/ITagQuery.cs
index e0c6a135c90e..8df363dbe447 100644
--- a/src/Umbraco.Core/PublishedCache/ITagQuery.cs
+++ b/src/Umbraco.Core/PublishedCache/ITagQuery.cs
@@ -33,7 +33,7 @@ public interface ITagQuery
///
/// Gets all document tags.
///
- /// ///
+ ///
/// If no culture is specified, it retrieves tags with an invariant culture.
/// If a culture is specified, it only retrieves tags for that culture.
/// Use "*" to retrieve tags for all cultures.
diff --git a/src/Umbraco.Core/PublishedCache/PublishedElement.cs b/src/Umbraco.Core/PublishedCache/PublishedElement.cs
index ab62c3777b68..9b0c6bcce664 100644
--- a/src/Umbraco.Core/PublishedCache/PublishedElement.cs
+++ b/src/Umbraco.Core/PublishedCache/PublishedElement.cs
@@ -1,3 +1,5 @@
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
@@ -16,9 +18,28 @@ public class PublishedElement : IPublishedElement
private readonly IPublishedProperty[] _propertiesArray;
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
+ public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing)
+ : this(contentType, key, values, previewing, PropertyCacheLevel.None, null)
+ {
+ }
+
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
+ public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, ICacheManager? cacheManager)
+ : this(
+ contentType,
+ key,
+ values,
+ previewing,
+ referenceCacheLevel,
+ StaticServiceProvider.Instance.GetRequiredService().VariationContext ?? new VariationContext(),
+ cacheManager)
+ {
+ }
+
// initializes a new instance of the PublishedElement class
// within the context of a published snapshot service (eg a published content property value)
- public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, ICacheManager? cacheManager)
+ public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, VariationContext variationContext, ICacheManager? cacheManager)
{
if (key == Guid.Empty)
{
@@ -40,7 +61,7 @@ public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary<
.Select(propertyType =>
{
values.TryGetValue(propertyType.Alias, out var value);
- return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel,cacheManager, value);
+ return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel, variationContext, cacheManager, value);
})
.ToArray()
?? [];
@@ -51,8 +72,8 @@ public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary<
// + using an initial reference cache level of .None ensures that everything will be
// cached at .Content level - and that reference cache level will propagate to all
// properties
- public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing)
- : this(contentType, key, values, previewing, PropertyCacheLevel.None, null)
+ public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing, VariationContext variationContext)
+ : this(contentType, key, values, previewing, PropertyCacheLevel.None, variationContext, null)
{
}
diff --git a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs
index f290afe5ea11..fb4ed21ef658 100644
--- a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs
+++ b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs
@@ -1,4 +1,3 @@
-using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
@@ -13,11 +12,11 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase
// to store eg routes, property converted values, anything - caching
// means faster execution, but uses memory - not sure if we want it
// so making it configurable.
- private const bool FullCacheWhenPreviewing = true;
private readonly Lock _locko = new();
private readonly object? _sourceValue;
protected readonly bool IsMember;
protected readonly bool IsPreviewing;
+ private readonly VariationContext _variationContext;
private readonly ICacheManager? _cacheManager;
private CacheValues? _cacheValues;
@@ -30,6 +29,7 @@ public PublishedElementPropertyBase(
IPublishedElement element,
bool previewing,
PropertyCacheLevel referenceCacheLevel,
+ VariationContext variationContext,
ICacheManager? cacheManager,
object? sourceValue = null)
: base(propertyType, referenceCacheLevel)
@@ -37,17 +37,22 @@ public PublishedElementPropertyBase(
_sourceValue = sourceValue;
Element = element;
IsPreviewing = previewing;
+ _variationContext = variationContext;
_cacheManager = cacheManager;
IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member;
}
// used to cache the CacheValues of this property
// ReSharper disable InconsistentlySynchronizedField
- internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(Element.Key, Alias, IsPreviewing);
+ private string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValuesKey();
+ [Obsolete("Do not use this. Will be removed in V17.")]
public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) =>
"PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]";
+ private string PropertyCacheValuesKey() =>
+ $"PublishedSnapshot.Property.CacheValues[{(IsPreviewing ? "D:" : "P:")}{Element.Key}:{Alias}:{_variationContext.Culture.IfNullOrWhiteSpace("inv")}+{_variationContext.Segment.IfNullOrWhiteSpace("inv")}]";
+
// ReSharper restore InconsistentlySynchronizedField
public override bool HasValue(string? culture = null, string? segment = null)
{
diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
index bbc0f4011b88..7f66f219332b 100644
--- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
@@ -89,8 +89,6 @@ public virtual IEnumerable GetOtherUrls(int id, Uri current)
yield break;
}
-
-
// look for domains, walking up the tree
IPublishedContent? n = node;
IEnumerable? domainUris =
diff --git a/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs
index d4059bcab806..c861d9ba010d 100644
--- a/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs
+++ b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
@@ -68,6 +68,12 @@ public async Task> GetAllAsync(IContent content)
urlInfos.Add(UrlInfo.Url(url, culture));
}
+ // If the content is trashed, we can't get the other URLs, as we have no parent structure to navigate through.
+ if (content.Trashed)
+ {
+ return urlInfos;
+ }
+
// Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains.
// for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them.
foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs
index 0c5ec59e755f..bd8eab7f8ece 100644
--- a/src/Umbraco.Core/Services/ContentPublishingService.cs
+++ b/src/Umbraco.Core/Services/ContentPublishingService.cs
@@ -239,7 +239,12 @@ private async Task ValidateCurrentContentAsync(IContent
}
///
+ [Obsolete("This method is not longer used as the 'force' parameter has been split into publishing unpublished and force re-published. Please use the overload containing parameters for those options instead. Will be removed in V17.")]
public async Task> PublishBranchAsync(Guid key, IEnumerable cultures, bool force, Guid userKey)
+ => await PublishBranchAsync(key, cultures, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, userKey);
+
+ ///
+ public async Task> PublishBranchAsync(Guid key, IEnumerable cultures, PublishBranchFilter publishBranchFilter, Guid userKey)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
IContent? content = _contentService.GetById(key);
@@ -260,7 +265,7 @@ public async Task result = _contentService.PublishBranch(content, force, cultures.ToArray(), userId);
+ IEnumerable result = _contentService.PublishBranch(content, publishBranchFilter, cultures.ToArray(), userId);
scope.Complete();
var itemResults = result.ToDictionary(r => r.Content.Key, ToContentPublishingOperationStatus);
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index ce1a724e4445..0ee108425239 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -2009,15 +2009,15 @@ private bool PublishBranch_PublishCultures(IContent content, HashSet cul
&& _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant());
}
- // utility 'ShouldPublish' func used by SaveAndPublishBranch
- private static HashSet? PublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force)
+ // utility 'ShouldPublish' func used by PublishBranch
+ private static HashSet? PublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, PublishBranchFilter publishBranchFilter)
{
// if published, republish
if (published)
{
cultures ??= new HashSet(); // empty means 'already published'
- if (edited)
+ if (edited || publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish))
{
cultures.Add(c); // means 'republish this culture'
}
@@ -2026,7 +2026,7 @@ private bool PublishBranch_PublishCultures(IContent content, HashSet cul
}
// if not published, publish if force/root else do nothing
- if (!force && !isRoot)
+ if (!publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && !isRoot)
{
return cultures; // null means 'nothing to do'
}
@@ -2054,16 +2054,18 @@ public IEnumerable SaveAndPublishBranch(IContent content, bool fo
var isRoot = c.Id == content.Id;
HashSet? culturesToPublish = null;
+ PublishBranchFilter publishBranchFilter = force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default;
+
// invariant content type
if (!c.ContentType.VariesByCulture())
{
- return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
+ return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
}
// variant content type, specific culture
if (culture != "*")
{
- return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
+ return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, publishBranchFilter);
}
// variant content type, all cultures
@@ -2073,7 +2075,7 @@ public IEnumerable SaveAndPublishBranch(IContent content, bool fo
// others will have to 'republish this culture'
foreach (var x in c.AvailableCultures)
{
- PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
+ PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter);
}
return culturesToPublish;
@@ -2085,23 +2087,31 @@ public IEnumerable SaveAndPublishBranch(IContent content, bool fo
: null; // null means 'nothing to do'
}
- return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId);
+ return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId);
}
[Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")]
public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
- => PublishBranch(content, force, cultures, userId);
+ => PublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId);
///
+ [Obsolete("This method is not longer used as the 'force' parameter has been split into publishing unpublished and force re-published. Please use the overload containing parameters for those options instead. Will be removed in V16")]
public IEnumerable PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
+ => PublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId);
+
+ ///
+ public IEnumerable PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
{
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
- cultures ??= Array.Empty();
- if (content.ContentType.VariesByCulture() is false && cultures.Length == 0)
+ cultures = EnsureCultures(content, cultures);
+
+ string? defaultCulture;
+ using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
- cultures = new[] { "*" };
+ defaultCulture = _languageRepository.GetDefaultIsoCode();
+ scope.Complete();
}
// determines cultures to be published
@@ -2114,7 +2124,7 @@ public IEnumerable PublishBranch(IContent content, bool force, st
// invariant content type
if (!c.ContentType.VariesByCulture())
{
- return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
+ return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
}
// variant content type, specific cultures
@@ -2122,26 +2132,42 @@ public IEnumerable PublishBranch(IContent content, bool force, st
{
// then some (and maybe all) cultures will be 'already published' (unless forcing),
// others will have to 'republish this culture'
- foreach (var x in cultures)
+ foreach (var culture in cultures)
{
- PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
+ // We could be publishing a parent invariant page, with descendents that are variant.
+ // So convert the invariant request to a request for the default culture.
+ var specificCulture = culture == "*" ? defaultCulture : culture;
+
+ PublishBranch_ShouldPublish(ref culturesToPublish, specificCulture, c.IsCulturePublished(specificCulture), c.IsCultureEdited(specificCulture), isRoot, publishBranchFilter);
}
return culturesToPublish;
}
- // if not published, publish if force/root else do nothing
- return force || isRoot
+ // if not published, publish if forcing unpublished/root else do nothing
+ return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot
? new HashSet(cultures) // means 'publish specified cultures'
: null; // null means 'nothing to do'
}
- return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId);
+ return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId);
}
+ private static string[] EnsureCultures(IContent content, string[] cultures)
+ {
+ // Ensure consistent indication of "all cultures" for variant content.
+ if (content.ContentType.VariesByCulture() is false && ProvidedCulturesIndicatePublishAll(cultures))
+ {
+ cultures = ["*"];
+ }
+
+ return cultures;
+ }
+
+ private static bool ProvidedCulturesIndicatePublishAll(string[] cultures) => cultures.Length == 0 || (cultures.Length == 1 && cultures[0] == "invariant");
+
internal IEnumerable PublishBranch(
IContent document,
- bool force,
Func?> shouldPublish,
Func, IReadOnlyCollection, bool> publishCultures,
int userId = Constants.Security.SuperUserId)
@@ -3116,7 +3142,7 @@ internal IEnumerable GetPublishedDescendantsLocked(IContent content)
{
var pathMatch = content.Path + ",";
IQuery query = Query()
- .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
+ .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& culture.Trashed == false*/);
IEnumerable contents = _documentRepository.Get(query);
// beware! contents contains all published version below content
diff --git a/src/Umbraco.Core/Services/IContentPublishingService.cs b/src/Umbraco.Core/Services/IContentPublishingService.cs
index 73fc6685435e..bf4102897796 100644
--- a/src/Umbraco.Core/Services/IContentPublishingService.cs
+++ b/src/Umbraco.Core/Services/IContentPublishingService.cs
@@ -3,6 +3,7 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Services.OperationStatus;
+using static Umbraco.Cms.Core.Constants.Conventions;
namespace Umbraco.Cms.Core.Services;
@@ -26,8 +27,22 @@ public interface IContentPublishingService
/// A value indicating whether to force-publish content that is not already published.
/// The identifier of the user performing the operation.
/// Result of the publish operation.
+ [Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead. Will be removed in V17.")]
Task> PublishBranchAsync(Guid key, IEnumerable cultures, bool force, Guid userKey);
+ ///
+ /// Publishes a content branch.
+ ///
+ /// The key of the root content.
+ /// The cultures to publish.
+ /// A value indicating options for force publishing unpublished or re-publishing unchanged content.
+ /// The identifier of the user performing the operation.
+ /// Result of the publish operation.
+ Task> PublishBranchAsync(Guid key, IEnumerable cultures, PublishBranchFilter publishBranchFilter, Guid userKey)
+#pragma warning disable CS0618 // Type or member is obsolete
+ => PublishBranchAsync(key, cultures, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), userKey);
+#pragma warning restore CS0618 // Type or member is obsolete
+
///
/// Unpublishes multiple cultures of a single content item.
///
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index dbc98680038b..0009a0d053c6 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -419,32 +419,25 @@ public interface IContentService : IContentServiceBase
/// published. The root of the branch is always published, regardless of .
///
///
+ [Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead. Will be removed in V17.")]
IEnumerable PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
- /////
- ///// Saves and publishes a document branch.
- /////
- ///// The root document.
- ///// A value indicating whether to force-publish documents that are not already published.
- ///// A function determining cultures to publish.
- ///// A function publishing cultures.
- ///// The identifier of the user performing the operation.
- /////
- ///// The parameter determines which documents are published. When false,
- ///// only those documents that are already published, are republished. When true, all documents are
- ///// published. The root of the branch is always published, regardless of .
- ///// The parameter is a function which determines whether a document has
- ///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
- ///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
- ///// cultures may trigger an unwanted republish.
- ///// The parameter is a function to execute to publish cultures, on
- ///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
- ///// whether the cultures could be published.
- /////
- // IEnumerable SaveAndPublishBranch(IContent content, bool force,
- // Func> shouldPublish,
- // Func, bool> publishCultures,
- // int userId = Constants.Security.SuperUserId);
+ ///
+ /// Publishes a document branch.
+ ///
+ /// The root document.
+ /// A value indicating options for force publishing unpublished or re-publishing unchanged content.
+ /// The cultures to publish.
+ /// The identifier of the user performing the operation.
+ ///
+ ///
+ /// The root of the branch is always published, regardless of .
+ ///
+ ///
+ IEnumerable PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
+#pragma warning disable CS0618 // Type or member is obsolete
+ => SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), cultures, userId);
+#pragma warning restore CS0618 // Type or member is obsolete
///
/// Unpublishes a document.
diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs
index 4cb17e9fbc49..710425e2c81a 100644
--- a/src/Umbraco.Core/Services/IMemberService.cs
+++ b/src/Umbraco.Core/Services/IMemberService.cs
@@ -217,6 +217,15 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
///
IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
+ ///
+ /// Saves an
+ ///
+ /// An can be of type or
+ /// or to Save
+ /// Enum for deciding which notifications to publish.
+ /// Id of the User saving the Member
+ Attempt Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId) => Save(member, userId);
+
///
/// Saves a single object
///
@@ -268,6 +277,21 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
///
IMember? GetById(int id);
+ ///
+ /// Get an list of for all members with the specified email.
+ ///
+ /// Email to use for retrieval
+ ///
+ ///
+ ///
+ IEnumerable GetMembersByEmail(string email)
+ =>
+ // TODO (V16): Remove this default implementation.
+ // The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
+ // in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
+ // does not override this method.
+ GetAllMembers().Where(x => x.Email.Equals(email));
+
///
/// Gets all Members for the specified MemberType alias
///
diff --git a/src/Umbraco.Core/Services/IWebhookLogService.cs b/src/Umbraco.Core/Services/IWebhookLogService.cs
index 12b53bfa7609..ee404e38e446 100644
--- a/src/Umbraco.Core/Services/IWebhookLogService.cs
+++ b/src/Umbraco.Core/Services/IWebhookLogService.cs
@@ -1,5 +1,4 @@
-using Umbraco.Cms.Core.Models;
-using Umbraco.Cms.Core.Webhooks;
+using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services;
@@ -8,4 +7,15 @@ public interface IWebhookLogService
Task CreateAsync(WebhookLog webhookLog);
Task> Get(int skip = 0, int take = int.MaxValue);
+
+ // TODO (V16): Remove the default implementation on this method.
+ async Task> Get(Guid webhookKey, int skip = 0, int take = int.MaxValue)
+ {
+ // This is very inefficient as the filter/skip/take is in-memory, but it will return the correct data.
+ // As it's only here to avoid a breaking change on the interface that is unlikely to have a custom
+ // implementation, this seems reasonable.
+ PagedModel allLogs = await Get(0, int.MaxValue);
+ var logsForId = allLogs.Items.Where(x => x.WebhookKey == webhookKey).ToList();
+ return new PagedModel(logsForId.Count, logsForId.Skip(skip).Take(take));
+ }
}
diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs
index 2c675063a2af..ee09f7d93a57 100644
--- a/src/Umbraco.Core/Services/MemberService.cs
+++ b/src/Umbraco.Core/Services/MemberService.cs
@@ -408,16 +408,23 @@ public IEnumerable GetAll(
}
///
- /// Get an by email
+ /// Get an by email. If RequireUniqueEmailForMembers is set to false, then the first member found with the specified email will be returned.
///
/// Email to use for retrieval
///
- public IMember? GetByEmail(string email)
+ public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
+
+ ///
+ /// Get an list of for all members with the specified email.
+ ///
+ /// Email to use for retrieval
+ ///
+ public IEnumerable GetMembersByEmail(string email)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(Constants.Locks.MemberTree);
IQuery query = Query().Where(x => x.Email.Equals(email));
- return _memberRepository.Get(query)?.FirstOrDefault();
+ return _memberRepository.Get(query);
}
///
@@ -765,6 +772,9 @@ public bool Exists(string username)
///
public Attempt Save(IMember member, int userId = Constants.Security.SuperUserId)
+ => Save(member, PublishNotificationSaveOptions.All, userId);
+
+ public Attempt Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId)
{
// trimming username and email to make sure we have no trailing space
member.Username = member.Username.Trim();
@@ -773,11 +783,15 @@ public bool Exists(string username)
EventMessages evtMsgs = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
- var savingNotification = new MemberSavingNotification(member, evtMsgs);
- if (scope.Notifications.PublishCancelable(savingNotification))
+ MemberSavingNotification? savingNotification = null;
+ if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saving))
{
- scope.Complete();
- return OperationResult.Attempt.Cancel(evtMsgs);
+ savingNotification = new MemberSavingNotification(member, evtMsgs);
+ if (scope.Notifications.PublishCancelable(savingNotification))
+ {
+ scope.Complete();
+ return OperationResult.Attempt.Cancel(evtMsgs);
+ }
}
if (string.IsNullOrWhiteSpace(member.Name))
@@ -789,7 +803,13 @@ public bool Exists(string username)
_memberRepository.Save(member);
- scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
+ if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
+ {
+ scope.Notifications.Publish(
+ savingNotification is null
+ ? new MemberSavedNotification(member, evtMsgs)
+ : new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
+ }
Audit(AuditType.Save, userId, member.Id);
diff --git a/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs
index ef16cd147183..2702b9434433 100644
--- a/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs
+++ b/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.Cms.Core.Services.OperationStatus;
+namespace Umbraco.Cms.Core.Services.OperationStatus;
///
/// Used to signal a user operation succeeded or an atomic failure reason
@@ -41,4 +41,5 @@ public enum UserOperationStatus
SelfPasswordResetNotAllowed,
DuplicateId,
InvalidUserType,
+ InvalidUserName,
}
diff --git a/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs b/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs
index 523bcdbfb144..c0478d1a5ad7 100644
--- a/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs
+++ b/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs
@@ -6,4 +6,11 @@ namespace Umbraco.Cms.Core.Services.Navigation;
public interface IPublishStatusQueryService
{
bool IsDocumentPublished(Guid documentKey, string culture);
+
+ ///
+ /// Checks if a document is published in any culture.
+ ///
+ /// Key to check for.
+ /// True if document has any published culture.
+ bool IsDocumentPublishedInAnyCulture(Guid documentKey) => IsDocumentPublished(documentKey, string.Empty);
}
diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
index c6d1f7481f58..98ea2c995872 100644
--- a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
+++ b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
@@ -76,6 +76,18 @@ public bool IsDocumentPublished(Guid documentKey, string culture)
return false;
}
+ ///
+ public bool IsDocumentPublishedInAnyCulture(Guid documentKey)
+ {
+ if (_publishedCultures.TryGetValue(documentKey, out ISet? publishedCultures))
+ {
+ return publishedCultures.Count > 0;
+ }
+
+ _logger.LogDebug("Document {DocumentKey} not found in the publish status cache", documentKey);
+ return false;
+ }
+
public async Task AddOrUpdateStatusAsync(Guid documentKey, CancellationToken cancellationToken)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs
index 657507f17b3d..e92a5b0cd0f2 100644
--- a/src/Umbraco.Core/Services/UserService.cs
+++ b/src/Umbraco.Core/Services/UserService.cs
@@ -1,13 +1,11 @@
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq.Expressions;
-using Microsoft.Extensions.DependencyInjection;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Editors;
@@ -40,7 +38,6 @@ internal partial class UserService : RepositoryService, IUserService
{
private readonly GlobalSettings _globalSettings;
private readonly SecuritySettings _securitySettings;
- private readonly ILogger _logger;
private readonly IUserGroupRepository _userGroupRepository;
private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper;
private readonly IServiceScopeFactory _serviceScopeFactory;
@@ -137,7 +134,6 @@ public UserService(
_globalSettings = globalSettings.Value;
_securitySettings = securitySettings.Value;
_contentSettings = contentSettings.Value;
- _logger = loggerFactory.CreateLogger();
}
///
@@ -966,6 +962,15 @@ private async Task ValidateUserCreateModel(UserCreateModel
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, existingUser);
}
+ // User names can only contain the configured allowed characters. This is validated by ASP.NET Identity on create
+ // as the setting is applied to the BackOfficeIdentityOptions, but we need to check ourselves for updates.
+ var allowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
+ if (model.UserName.Any(c => allowedUserNameCharacters.Contains(c) == false))
+ {
+ scope.Complete();
+ return Attempt.FailWithStatus(UserOperationStatus.InvalidUserName, existingUser);
+ }
+
IEnumerable allUserGroups = _userGroupRepository.GetMany().ToArray();
var userGroups = allUserGroups.Where(x => model.UserGroupKeys.Contains(x.Key)).ToHashSet();
diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs
index e599a6579252..bba70566a595 100644
--- a/src/Umbraco.Core/Services/UserServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs
@@ -87,6 +87,6 @@ public static IEnumerable GetProfilesById(this IUserService userServic
});
}
- [Obsolete("Use IUserService.Get that takes a Guid instead. Scheduled for removal in V15.")]
+ [Obsolete("Use IUserService.GetAsync that takes a Guid instead. Scheduled for removal in V15.")]
public static IUser? GetByKey(this IUserService userService, Guid key) => userService.GetAsync(key).GetAwaiter().GetResult();
}
diff --git a/src/Umbraco.Core/Services/WebhookLogService.cs b/src/Umbraco.Core/Services/WebhookLogService.cs
index 3b0bbebf1997..4257b775d4d1 100644
--- a/src/Umbraco.Core/Services/WebhookLogService.cs
+++ b/src/Umbraco.Core/Services/WebhookLogService.cs
@@ -1,7 +1,6 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
-using Umbraco.Cms.Core.Webhooks;
namespace Umbraco.Cms.Core.Services;
@@ -30,4 +29,10 @@ public async Task> Get(int skip = 0, int take = int.MaxVa
using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true);
return await _webhookLogRepository.GetPagedAsync(skip, take);
}
+
+ public async Task> Get(Guid webhookKey, int skip = 0, int take = int.MaxValue)
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true);
+ return await _webhookLogRepository.GetPagedAsync(webhookKey, skip, take);
+ }
}
diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs
index ac3fabd97386..f6eff0b315d4 100644
--- a/src/Umbraco.Core/Services/WebhookService.cs
+++ b/src/Umbraco.Core/Services/WebhookService.cs
@@ -88,6 +88,8 @@ public async Task> UpdateAsync(IWebhoo
currentWebhook.Enabled = webhook.Enabled;
currentWebhook.ContentTypeKeys = webhook.ContentTypeKeys;
currentWebhook.Events = webhook.Events;
+ currentWebhook.Name = webhook.Name;
+ currentWebhook.Description = webhook.Description;
currentWebhook.Url = webhook.Url;
currentWebhook.Headers = webhook.Headers;
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs
index e6d25aff0e2a..07d8a09d9adc 100644
--- a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs
@@ -51,6 +51,12 @@ public WebhookFiring(
public async Task RunJobAsync()
{
+ if (_webhookSettings.Enabled is false)
+ {
+ _logger.LogInformation("WebhookFiring task will not run as it has been globally disabled via configuration");
+ return;
+ }
+
IEnumerable requests;
using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
{
diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
index 7f7f8d678422..9494ed2eea58 100644
--- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
+++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
@@ -24,6 +24,8 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
private readonly RepositoryCachePolicyOptions _options;
+ private const string NullRepresentationInCache = "*NULL*";
+
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
: base(cache, scopeAccessor) =>
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -116,6 +118,7 @@ public override void Delete(TEntity entity, Action persistDeleted)
{
// whatever happens, clear the cache
var cacheKey = GetEntityCacheKey(entity.Id);
+
Cache.Clear(cacheKey);
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
@@ -127,20 +130,36 @@ public override void Delete(TEntity entity, Action persistDeleted)
public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll)
{
var cacheKey = GetEntityCacheKey(id);
+
TEntity? fromCache = Cache.GetCacheItem(cacheKey);
- // if found in cache then return else fetch and cache
- if (fromCache != null)
+ // If found in cache then return immediately.
+ if (fromCache is not null)
{
return fromCache;
}
+ // Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
+ // Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist.
+ // If we've cached a "null" value, return null.
+ if (_options.CacheNullValues && Cache.GetCacheItem(cacheKey) == NullRepresentationInCache)
+ {
+ return null;
+ }
+
+ // Otherwise go to the database to retrieve.
TEntity? entity = performGet(id);
if (entity != null && entity.HasIdentity)
{
+ // If we've found an identified entity, cache it for subsequent retrieval.
InsertEntity(cacheKey, entity);
}
+ else if (entity is null && _options.CacheNullValues)
+ {
+ // If we've not found an entity, and we're caching null values, cache a "null" value.
+ InsertNull(cacheKey);
+ }
return entity;
}
@@ -248,6 +267,15 @@ protected string GetEntityCacheKey(TId? id)
protected virtual void InsertEntity(string cacheKey, TEntity entity)
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
+ protected virtual void InsertNull(string cacheKey)
+ {
+ // We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
+ // a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
+ // Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
+ // So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
+ Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
+ }
+
protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
{
if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index c70616bfd4c3..599b2fb9d3b6 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -104,7 +104,12 @@ protected virtual void DefinePlan()
To("{9D3CE7D4-4884-41D4-98E8-302EB6CB0CF6}");
To("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}");
To("{42E44F9E-7262-4269-922D-7310CB48E724}");
+
+ // To 15.1.0
To("{7B51B4DE-5574-4484-993E-05D12D9ED703}");
To("{F3D3EF46-1B1F-47DB-B437-7D573EEDEB98}");
+
+ // To 15.3.0
+ To("{7B11F01E-EE33-4B0B-81A1-F78F834CA45B}");
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs
index 6ee48ce0e7a3..f45b5d371b66 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs
@@ -1,4 +1,4 @@
-using NPoco;
+using NPoco;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
@@ -153,16 +153,26 @@ JOIN sys.columns columns
");
var currentConstraintName = Database.ExecuteScalar(constraintNameQuery);
-
- // only rename the constraint if necessary
+ // Only rename the constraint if necessary.
if (currentConstraintName == expectedConstraintName)
{
return;
}
- Sql renameConstraintQuery = Database.SqlContext.Sql(
- $"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
- Database.Execute(renameConstraintQuery);
+ if (currentConstraintName is null)
+ {
+ // Constraint does not exist, so we need to create it.
+ Sql createConstraintStatement = Database.SqlContext.Sql(@$"
+ALTER TABLE umbracoContentVersion ADD CONSTRAINT [DF_umbracoContentVersion_versionDate] DEFAULT (getdate()) FOR [versionDate]");
+ Database.Execute(createConstraintStatement);
+ }
+ else
+ {
+ // Constraint exists, and differs from the expected name, so we need to rename it.
+ Sql renameConstraintQuery = Database.SqlContext.Sql(
+ $"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
+ Database.Execute(renameConstraintQuery);
+ }
}
private void UpdateExternalLoginIndexes(IEnumerable> indexes)
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_3_0/AddNameAndDescriptionToWebhooks.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_3_0/AddNameAndDescriptionToWebhooks.cs
new file mode 100644
index 000000000000..eee8d5e26cd4
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_3_0/AddNameAndDescriptionToWebhooks.cs
@@ -0,0 +1,43 @@
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_3_0;
+
+public class AddNameAndDescriptionToWebhooks : MigrationBase
+{
+ private readonly ILogger _logger;
+
+ public AddNameAndDescriptionToWebhooks(IMigrationContext context, ILogger logger)
+ : base(context)
+ {
+ _logger = logger;
+ }
+
+ protected override void Migrate()
+ {
+ Logger.LogDebug("Adding name and description columns to webhooks.");
+
+ if (TableExists(Constants.DatabaseSchema.Tables.Webhook))
+ {
+ var columns = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumn(columns, "name");
+ AddColumn(columns, "description");
+ }
+ else
+ {
+ Logger.LogWarning($"Table {Constants.DatabaseSchema.Tables.Webhook} does not exist so the addition of the name and description by columnss in migration {nameof(AddNameAndDescriptionToWebhooks)} cannot be completed.");
+ }
+ }
+
+ private void AddColumn(List columns, string column)
+ {
+ if (columns
+ .SingleOrDefault(x => x.TableName == Constants.DatabaseSchema.Tables.Webhook && x.ColumnName == column) is null)
+ {
+ AddColumn(Constants.DatabaseSchema.Tables.Webhook, column);
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs b/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs
index 9904ea33bb73..cf64747ec2ff 100644
--- a/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs
+++ b/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs
@@ -10,5 +10,5 @@ public class RichTextEditorValue
public required string Markup { get; set; }
[DataMember(Name = "blocks")]
- public required RichTextBlockValue? Blocks { get; set; }
+ public RichTextBlockValue? Blocks { get; set; }
}
diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
index a2d24badbe4a..81e08a8d591b 100644
--- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
+++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
@@ -143,14 +143,17 @@ public void WriteClrType(StringBuilder sb, Type type)
//
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
- private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
+ private void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
- tabs, ApiVersion.Current.Version);
+ tabs,
+ Config.IncludeVersionNumberInGeneratedModels ? ApiVersion.Current.Version : null);
// writes an attribute that specifies that an output may be null.
// (useful for consuming projects with nullable reference types enabled)
private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
- sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
+ sb.AppendFormat(
+ "{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n",
+ tabs,
isReturn ? "return: " : string.Empty);
private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs
index 2fb0d1355557..dac753fb068c 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs
@@ -1,4 +1,4 @@
-using NPoco;
+using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
@@ -18,6 +18,15 @@ internal class WebhookDto
[NullSetting(NullSetting = NullSettings.NotNull)]
public Guid Key { get; set; }
+ [Column(Name = "name")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public string? Name { get; set; }
+
+ [Column(Name = "description")]
+ [SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public string? Description { get; set; }
+
[Column(Name = "url")]
[SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
[NullSetting(NullSetting = NullSettings.NotNull)]
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs
index ad081b5bdafb..3b559f5227da 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs
@@ -1,4 +1,4 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Persistence.Factories;
@@ -16,6 +16,8 @@ public static Webhook BuildEntity(WebhookDto dto, IEnumerable CreateCachePolic
var options = new RepositoryCachePolicyOptions
{
// allow zero to be cached
- GetAllCacheAllowZeroCount = true,
+ GetAllCacheAllowZeroCount = true
};
- return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor,
- options);
+ return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options);
}
private IDictionaryItem ConvertFromDto(DictionaryDto dto, IDictionary languagesById)
@@ -217,11 +216,10 @@ protected override IRepositoryCachePolicy CreateCachePoli
var options = new RepositoryCachePolicyOptions
{
// allow zero to be cached
- GetAllCacheAllowZeroCount = true,
+ GetAllCacheAllowZeroCount = true
};
- return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor,
- options);
+ return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options);
}
protected override IEnumerable PerformGetAll(params Guid[]? ids)
@@ -272,12 +270,13 @@ protected override IRepositoryCachePolicy CreateCachePo
{
var options = new RepositoryCachePolicyOptions
{
+ // allow null to be cached
+ CacheNullValues = true,
// allow zero to be cached
- GetAllCacheAllowZeroCount = true,
+ GetAllCacheAllowZeroCount = true
};
- return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor,
- options);
+ return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options);
}
protected override IEnumerable PerformGetAll(params string[]? ids)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
index 0c2980a2ab21..af28b8325c94 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
@@ -389,7 +389,9 @@ private static IEnumerable Map(IEnumerable dtos)
}).ToList();
///
- public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null,
+ public IEnumerable GetTagsForEntityType(
+ TaggableObjectTypes objectType,
+ string? group = null,
string? culture = null)
{
Sql sql = GetTagsSql(culture, true);
@@ -403,6 +405,9 @@ public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, st
.Where(dto => dto.NodeObjectType == nodeObjectType);
}
+ sql = sql
+ .Where(dto => !dto.Trashed);
+
if (group.IsNullOrWhiteSpace() == false)
{
sql = sql
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs
index af6c561d9031..334407d01842 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs
@@ -1,4 +1,4 @@
-using NPoco;
+using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
@@ -37,10 +37,17 @@ public async Task CreateAsync(WebhookLog log)
}
public async Task> GetPagedAsync(int skip, int take)
+ => await GetPagedAsyncInternal(null, skip, take);
+
+ public async Task> GetPagedAsync(Guid webhookKey, int skip, int take)
+ => await GetPagedAsyncInternal(webhookKey, skip, take);
+
+ private async Task> GetPagedAsyncInternal(Guid? webhookKey, int skip, int take)
{
Sql sql = Database.SqlContext.Sql()
.Select()
.From()
+ .Where(x => !webhookKey.HasValue || x.WebhookKey == webhookKey)
.OrderByDescending(x => x.Date);
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
index 08a8bb865dd0..22e9384bf201 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
@@ -296,6 +296,18 @@ private void MapBlockItemDataFromEditor(List items)
BlockEditorData? source = BlockEditorValues.DeserializeAndClean(sourceValue);
BlockEditorData? target = BlockEditorValues.DeserializeAndClean(targetValue);
+ TValue? mergedBlockValue =
+ MergeVariantInvariantPropertyValueTyped(source, target, canUpdateInvariantData, allowedCultures);
+
+ return _jsonSerializer.Serialize(mergedBlockValue);
+ }
+
+ internal virtual TValue? MergeVariantInvariantPropertyValueTyped(
+ BlockEditorData? source,
+ BlockEditorData? target,
+ bool canUpdateInvariantData,
+ HashSet allowedCultures)
+ {
source = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
if (source is null && target is null)
@@ -320,15 +332,15 @@ private void MapBlockItemDataFromEditor(List items)
// remove all the blocks that are no longer part of the layout
target.BlockValue.ContentData.RemoveAll(contentBlock =>
- target.Layout!.Any(layoutItem => layoutItem.ContentKey == contentBlock.Key) is false);
+ target.Layout!.Any(layoutItem => layoutItem.ReferencesContent(contentBlock.Key)) is false);
target.BlockValue.SettingsData.RemoveAll(settingsBlock =>
- target.Layout!.Any(layoutItem => layoutItem.SettingsKey == settingsBlock.Key) is false);
+ target.Layout!.Any(layoutItem => layoutItem.ReferencesSetting(settingsBlock.Key)) is false);
CleanupVariantValues(source.BlockValue.ContentData, target.BlockValue.ContentData, canUpdateInvariantData, allowedCultures);
CleanupVariantValues(source.BlockValue.SettingsData, target.BlockValue.SettingsData, canUpdateInvariantData, allowedCultures);
- return _jsonSerializer.Serialize(target.BlockValue);
+ return target.BlockValue;
}
private void CleanupVariantValues(
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
index 239d4652bb53..d8bc7e55530f 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
@@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Exceptions;
@@ -33,6 +34,8 @@ public sealed class RichTextEditorPastedImages
private readonly IScopeProvider _scopeProvider;
private readonly IMediaImportService _mediaImportService;
private readonly IImageUrlGenerator _imageUrlGenerator;
+ private readonly IEntityService _entityService;
+ private readonly AppCaches _appCaches;
private readonly IUserService _userService;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
@@ -84,6 +87,30 @@ public RichTextEditorPastedImages(
{
}
+ // highest overload to be picked by DI, pointing to newest ctor
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
+ public RichTextEditorPastedImages(
+ IUmbracoContextAccessor umbracoContextAccessor,
+ ILogger logger,
+ IHostingEnvironment hostingEnvironment,
+ IMediaService mediaService,
+ IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
+ MediaFileManager mediaFileManager,
+ MediaUrlGeneratorCollection mediaUrlGenerators,
+ IShortStringHelper shortStringHelper,
+ IPublishedUrlProvider publishedUrlProvider,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider,
+ IMediaImportService mediaImportService,
+ IImageUrlGenerator imageUrlGenerator,
+ IOptions contentSettings,
+ IEntityService entityService,
+ AppCaches appCaches)
+ : this(umbracoContextAccessor, publishedUrlProvider, temporaryFileService, scopeProvider, mediaImportService, imageUrlGenerator, entityService, appCaches)
+ {
+ }
+
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedUrlProvider publishedUrlProvider,
@@ -91,6 +118,27 @@ public RichTextEditorPastedImages(
IScopeProvider scopeProvider,
IMediaImportService mediaImportService,
IImageUrlGenerator imageUrlGenerator)
+ : this(
+ umbracoContextAccessor,
+ publishedUrlProvider,
+ temporaryFileService,
+ scopeProvider,
+ mediaImportService,
+ imageUrlGenerator,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ public RichTextEditorPastedImages(
+ IUmbracoContextAccessor umbracoContextAccessor,
+ IPublishedUrlProvider publishedUrlProvider,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider,
+ IMediaImportService mediaImportService,
+ IImageUrlGenerator imageUrlGenerator,
+ IEntityService entityService,
+ AppCaches appCaches)
{
_umbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
@@ -99,6 +147,8 @@ public RichTextEditorPastedImages(
_scopeProvider = scopeProvider;
_mediaImportService = mediaImportService;
_imageUrlGenerator = imageUrlGenerator;
+ _entityService = entityService;
+ _appCaches = appCaches;
// this obviously is not correct. however, we only use IUserService in an obsolete method,
// so this is better than having even more obsolete constructors for V16
@@ -161,7 +211,7 @@ public async Task FindAndPersistPastedTempImagesAsync(string html, Guid
if (uploadedImages.ContainsKey(temporaryFileKey) == false)
{
using Stream fileStream = temporaryFile.OpenReadStream();
- Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? Constants.System.RootKey : mediaParentFolder;
+ Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? await GetDefaultMediaRoot(userKey) : mediaParentFolder;
IMedia mediaFile = await _mediaImportService.ImportAsync(temporaryFile.FileName, fileStream, parentFolderKey, MediaTypeAlias(temporaryFile.FileName), userKey);
udi = mediaFile.GetUdi();
}
@@ -214,6 +264,20 @@ public async Task FindAndPersistPastedTempImagesAsync(string html, Guid
return htmlDoc.DocumentNode.OuterHtml;
}
+ private async Task GetDefaultMediaRoot(Guid userKey)
+ {
+ IUser user = await _userService.GetAsync(userKey) ?? throw new ArgumentException("User could not be found");
+ var userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
+ var firstNodeId = userStartNodes?.FirstOrDefault();
+ if (firstNodeId is null)
+ {
+ return Constants.System.RootKey;
+ }
+
+ Attempt firstNodeKeyAttempt = _entityService.GetKey(firstNodeId.Value, UmbracoObjectTypes.Media);
+ return firstNodeKeyAttempt.Success ? firstNodeKeyAttempt.Result : Constants.System.RootKey;
+ }
+
private string MediaTypeAlias(string fileName)
=> fileName.InvariantEndsWith(".svg")
? Constants.Conventions.MediaTypes.VectorGraphicsAlias
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index b2e4d21a9633..92c0a05e940b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -92,7 +92,6 @@ internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase _logger;
@@ -122,12 +121,12 @@ public RichTextPropertyValueEditor(
_localLinkParser = localLinkParser;
_pastedImages = pastedImages;
_htmlSanitizer = htmlSanitizer;
- _elementTypeCache = elementTypeCache;
_richTextRequiredValidator = richTextRequiredValidator;
_jsonSerializer = jsonSerializer;
_logger = logger;
- Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), elementTypeCache, jsonSerializer, logger));
+ BlockEditorValues = new(new RichTextEditorBlockDataConverter(_jsonSerializer), elementTypeCache, logger);
+ Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, BlockEditorValues, elementTypeCache, jsonSerializer, logger));
}
public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator;
@@ -275,6 +274,70 @@ public override IEnumerable ConfiguredElementTypeKeys()
return configuration?.Blocks?.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty();
}
+ internal override object? MergeVariantInvariantPropertyValue(
+ object? sourceValue,
+ object? targetValue,
+ bool canUpdateInvariantData,
+ HashSet allowedCultures)
+ {
+ TryParseEditorValue(sourceValue, out RichTextEditorValue? sourceRichTextEditorValue);
+ TryParseEditorValue(targetValue, out RichTextEditorValue? targetRichTextEditorValue);
+
+ var mergedBlockValue = MergeBlockVariantInvariantData(
+ sourceRichTextEditorValue?.Blocks,
+ targetRichTextEditorValue?.Blocks,
+ canUpdateInvariantData,
+ allowedCultures);
+
+ var mergedMarkupValue = MergeMarkupValue(
+ sourceRichTextEditorValue?.Markup ?? string.Empty,
+ targetRichTextEditorValue?.Markup ?? string.Empty,
+ mergedBlockValue,
+ canUpdateInvariantData);
+
+ var mergedEditorValue = new RichTextEditorValue { Markup = mergedMarkupValue, Blocks = mergedBlockValue };
+ return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(mergedEditorValue, _jsonSerializer);
+ }
+
+ private string MergeMarkupValue(
+ string source,
+ string target,
+ RichTextBlockValue? mergedBlockValue,
+ bool canUpdateInvariantData)
+ {
+ // pick source or target based on culture permissions
+ var mergedMarkup = canUpdateInvariantData ? target : source;
+
+ // todo? strip all invalid block links from markup, those tat are no longer in the layout
+ return mergedMarkup;
+ }
+
+ private RichTextBlockValue? MergeBlockVariantInvariantData(
+ RichTextBlockValue? sourceRichTextBlockValue,
+ RichTextBlockValue? targetRichTextBlockValue,
+ bool canUpdateInvariantData,
+ HashSet allowedCultures)
+ {
+ if (sourceRichTextBlockValue is null && targetRichTextBlockValue is null)
+ {
+ return null;
+ }
+
+ BlockEditorData sourceBlockEditorData =
+ (sourceRichTextBlockValue is not null ? ConvertAndClean(sourceRichTextBlockValue) : null)
+ ?? new BlockEditorData([], new RichTextBlockValue());
+
+ BlockEditorData