From 2c04e37b42310a55e06dc5381e8ee4431bed2b85 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 5 Nov 2024 06:32:34 +0100 Subject: [PATCH] Support limited language access at block level (#17322) * Support limited language access at block level * Account for AllowEditInvariantFromNonDefault when updating properties (#17333) * Remove obsolete ctor * Add explanatory comment * Set AllowEditInvariantFromNonDefault to true on tests * Refactor to account for merge and default language * Merge invariant values on top of the already merged values * Add integration test to prove invariant merging --------- Co-authored-by: kjac --------- Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Services/ContentEditingService.cs | 79 ++- .../BlockEditorElementVariationTestBase.cs | 2 + ...kListElementLevelVariationTests.Editing.cs | 501 ++++++++++++++++++ .../BlockListElementLevelVariationTests.cs | 6 + .../Umbraco.Tests.Integration.csproj | 3 + 5 files changed, 551 insertions(+), 40 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 2a7e49dbda..c1df8c2f77 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; @@ -14,42 +14,13 @@ namespace Umbraco.Cms.Core.Services; internal sealed class ContentEditingService : ContentEditingServiceWithSortingBase, IContentEditingService { + private readonly PropertyEditorCollection _propertyEditorCollection; private readonly ITemplateService _templateService; private readonly ILogger _logger; private readonly IUserService _userService; private readonly ILocalizationService _localizationService; private readonly ILanguageService _languageService; - - [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")] - public ContentEditingService( - IContentService contentService, - IContentTypeService contentTypeService, - PropertyEditorCollection propertyEditorCollection, - IDataTypeService dataTypeService, - ITemplateService templateService, - ILogger logger, - ICoreScopeProvider scopeProvider, - IUserIdKeyResolver userIdKeyResolver, - ITreeEntitySortingService treeEntitySortingService, - IContentValidationService contentValidationService) - : this( - contentService, - contentTypeService, - propertyEditorCollection, - dataTypeService, - templateService, - logger, - scopeProvider, - userIdKeyResolver, - treeEntitySortingService, - contentValidationService, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService() - ) - { - - } + private readonly ContentSettings _contentSettings; public ContentEditingService( IContentService contentService, @@ -64,14 +35,17 @@ internal sealed class ContentEditingService IContentValidationService contentValidationService, IUserService userService, ILocalizationService localizationService, - ILanguageService languageService) + ILanguageService languageService, + IOptions contentSettings) : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService) { + _propertyEditorCollection = propertyEditorCollection; _templateService = templateService; _logger = logger; _userService = userService; _localizationService = localizationService; _languageService = languageService; + _contentSettings = contentSettings.Value; } public async Task GetAsync(Guid key) @@ -154,6 +128,8 @@ internal sealed class ContentEditingService var allowedCultures = (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet(); + ILanguage? defaultLanguage = await _languageService.GetDefaultLanguageAsync(); + foreach (var culture in contentWithPotentialUnallowedChanges.EditedCultures ?? contentWithPotentialUnallowedChanges.PublishedCultures) { if (allowedCultures.Contains(culture)) @@ -161,21 +137,44 @@ internal sealed class ContentEditingService continue; } - // else override the updates values with the original values. foreach (IProperty property in contentWithPotentialUnallowedChanges.Properties) { - if (property.PropertyType.VariesByCulture() is false) + // if the property varies by culture, simply overwrite the edited property value with the current property value + if (property.PropertyType.VariesByCulture()) { + var currentValue = existingContent?.Properties.First(x => x.Alias == property.Alias).GetValue(culture, null, false); + property.SetValue(currentValue, culture, null); continue; } - var value = existingContent?.Properties.First(x=>x.Alias == property.Alias).GetValue(culture, null, false); - property.SetValue(value, culture, null); + // if the property does not vary by culture and the data editor supports variance within invariant property values, + // we need perform a merge between the edited property value and the current property value + if (_propertyEditorCollection.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) && dataEditor.CanMergePartialPropertyValues(property.PropertyType)) + { + var currentValue = existingContent?.Properties.First(x => x.Alias == property.Alias).GetValue(null, null, false); + var editedValue = contentWithPotentialUnallowedChanges.Properties.First(x => x.Alias == property.Alias).GetValue(null, null, false); + var mergedValue = dataEditor.MergePartialPropertyValueForCulture(currentValue, editedValue, culture); + + // If we are not allowed to edit invariant properties, overwrite the edited property value with the current property value. + if (_contentSettings.AllowEditInvariantFromNonDefault is false && culture == defaultLanguage?.IsoCode) + { + mergedValue = dataEditor.MergePartialPropertyValueForCulture(currentValue, mergedValue, null); + } + + property.SetValue(mergedValue, null, null); + } + + // If property does not support merging, we still need to overwrite if we are not allowed to edit invariant properties. + else if (_contentSettings.AllowEditInvariantFromNonDefault is false && culture == defaultLanguage?.IsoCode) + { + var currentValue = existingContent?.Properties.First(x => x.Alias == property.Alias).GetValue(null, null, false); + property.SetValue(currentValue, null, null); + } } } - return contentWithPotentialUnallowedChanges; + return contentWithPotentialUnallowedChanges; } public async Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs index 74e7ea77a9..83bd01e074 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs @@ -37,6 +37,8 @@ public abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTe protected PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + protected IContentEditingService ContentEditingService => GetRequiredService(); + private IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService(); private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs new file mode 100644 index 0000000000..2cdadc5e46 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Editing.cs @@ -0,0 +1,501 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +public partial class BlockListElementLevelVariationTests +{ + [TestCase(true)] + [TestCase(false)] + public async Task Can_Handle_Limited_User_Access_To_Languages_With_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess) + { + await LanguageService.CreateAsync( + new Language("de-DE", "German"), Constants.Security.SuperUserKey); + var userKey = updateWithLimitedUserAccess + ? (await CreateLimitedUser()).Key + : Constants.Security.SuperUserKey; + + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var content = CreateContent(contentType, elementType, [], false); + content.SetCultureName("Home (de)", "de-DE"); + ContentService.Save(content); + + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" } + }, + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" } + }, + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" } + }, + new List { + new() { Alias = "invariantText", Value = "#2: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" } + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + blockListValue.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "#1: The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + blockListValue.ContentData[0].Values[3].Value = "#1: The second content value in German"; + blockListValue.SettingsData[0].Values[0].Value = "#1: The second invariant settings value"; + blockListValue.SettingsData[0].Values[1].Value = "#1: The second settings value in English"; + blockListValue.SettingsData[0].Values[2].Value = "#1: The second settings value in Danish"; + blockListValue.SettingsData[0].Values[3].Value = "#1: The second settings value in German"; + + blockListValue.ContentData[1].Values[0].Value = "#2: The second invariant content value"; + blockListValue.ContentData[1].Values[1].Value = "#2: The second content value in English"; + blockListValue.ContentData[1].Values[2].Value = "#2: The second content value in Danish"; + blockListValue.ContentData[1].Values[3].Value = "#2: The second content value in German"; + blockListValue.SettingsData[1].Values[0].Value = "#2: The second invariant settings value"; + blockListValue.SettingsData[1].Values[1].Value = "#2: The second settings value in English"; + blockListValue.SettingsData[1].Values[2].Value = "#2: The second settings value in Danish"; + blockListValue.SettingsData[1].Values[3].Value = "#2: The second settings value in German"; + + var updateModel = new ContentUpdateModel + { + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + }, + Variants = new[] + { + new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] }, + new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] }, + new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] } + } + }; + + var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey); + Assert.IsTrue(result.Success); + + content = ContentService.GetById(content.Key); + var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString(); + Assert.NotNull(savedBlocksValue); + blockListValue = JsonSerializer.Deserialize(savedBlocksValue); + + // the Danish and invariant values should be updated regardless of the executing user + Assert.Multiple(() => + { + Assert.AreEqual("#1: The second invariant content value", blockListValue.ContentData[0].Values[0].Value); + Assert.AreEqual("#1: The second content value in Danish", blockListValue.ContentData[0].Values[2].Value); + Assert.AreEqual("#1: The second invariant settings value", blockListValue.SettingsData[0].Values[0].Value); + Assert.AreEqual("#1: The second settings value in Danish", blockListValue.SettingsData[0].Values[2].Value); + + Assert.AreEqual("#2: The second invariant content value", blockListValue.ContentData[1].Values[0].Value); + Assert.AreEqual("#2: The second content value in Danish", blockListValue.ContentData[1].Values[2].Value); + Assert.AreEqual("#2: The second invariant settings value", blockListValue.SettingsData[1].Values[0].Value); + Assert.AreEqual("#2: The second settings value in Danish", blockListValue.SettingsData[1].Values[2].Value); + }); + + // limited user access means English and German should not have been updated - changes should be rolled back to the initial block values + if (updateWithLimitedUserAccess) + { + Assert.Multiple(() => + { + Assert.AreEqual("#1: The first content value in English", blockListValue.ContentData[0].Values[1].Value); + Assert.AreEqual("#1: The first settings value in English", blockListValue.SettingsData[0].Values[1].Value); + Assert.AreEqual("#1: The first content value in German", blockListValue.ContentData[0].Values[3].Value); + Assert.AreEqual("#1: The first settings value in German", blockListValue.SettingsData[0].Values[3].Value); + + Assert.AreEqual("#2: The first content value in English", blockListValue.ContentData[1].Values[1].Value); + Assert.AreEqual("#2: The first settings value in English", blockListValue.SettingsData[1].Values[1].Value); + Assert.AreEqual("#2: The first content value in German", blockListValue.ContentData[1].Values[3].Value); + Assert.AreEqual("#2: The first settings value in German", blockListValue.SettingsData[1].Values[3].Value); + }); + } + else + { + Assert.Multiple(() => + { + Assert.AreEqual("#1: The second content value in English", blockListValue.ContentData[0].Values[1].Value); + Assert.AreEqual("#1: The second settings value in English", blockListValue.SettingsData[0].Values[1].Value); + Assert.AreEqual("#1: The second content value in German", blockListValue.ContentData[0].Values[3].Value); + Assert.AreEqual("#1: The second settings value in German", blockListValue.SettingsData[0].Values[3].Value); + + Assert.AreEqual("#2: The second content value in English", blockListValue.ContentData[1].Values[1].Value); + Assert.AreEqual("#2: The second settings value in English", blockListValue.SettingsData[1].Values[1].Value); + Assert.AreEqual("#2: The second content value in German", blockListValue.ContentData[1].Values[3].Value); + Assert.AreEqual("#2: The second settings value in German", blockListValue.SettingsData[1].Values[3].Value); + }); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Handle_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess) + { + await LanguageService.CreateAsync( + new Language("de-DE", "German"), Constants.Security.SuperUserKey); + var userKey = updateWithLimitedUserAccess + ? (await CreateLimitedUser()).Key + : Constants.Security.SuperUserKey; + + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var content = CreateContent(contentType, elementType, [], false); + content.SetCultureName("Home (de)", "de-DE"); + ContentService.Save(content); + + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" } + }, + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" } + }, + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" } + }, + new List { + new() { Alias = "invariantText", Value = "#2: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" } + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + blockListValue.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "#1: The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + blockListValue.ContentData[0].Values[3].Value = "#1: The second content value in German"; + blockListValue.SettingsData[0].Values[0].Value = "#1: The second invariant settings value"; + blockListValue.SettingsData[0].Values[1].Value = "#1: The second settings value in English"; + blockListValue.SettingsData[0].Values[2].Value = "#1: The second settings value in Danish"; + blockListValue.SettingsData[0].Values[3].Value = "#1: The second settings value in German"; + + blockListValue.ContentData[1].Values[0].Value = "#2: The second invariant content value"; + blockListValue.ContentData[1].Values[1].Value = "#2: The second content value in English"; + blockListValue.ContentData[1].Values[2].Value = "#2: The second content value in Danish"; + blockListValue.ContentData[1].Values[3].Value = "#2: The second content value in German"; + blockListValue.SettingsData[1].Values[0].Value = "#2: The second invariant settings value"; + blockListValue.SettingsData[1].Values[1].Value = "#2: The second settings value in English"; + blockListValue.SettingsData[1].Values[2].Value = "#2: The second settings value in Danish"; + blockListValue.SettingsData[1].Values[3].Value = "#2: The second settings value in German"; + + var updateModel = new ContentUpdateModel + { + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + }, + Variants = new[] + { + new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] }, + new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] }, + new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] } + } + }; + + var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey); + Assert.IsTrue(result.Success); + + content = ContentService.GetById(content.Key); + var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString(); + Assert.NotNull(savedBlocksValue); + blockListValue = JsonSerializer.Deserialize(savedBlocksValue); + + // the Danish values should be updated regardless of the executing user + Assert.Multiple(() => + { + Assert.AreEqual("#1: The second content value in Danish", blockListValue.ContentData[0].Values[2].Value); + Assert.AreEqual("#1: The second settings value in Danish", blockListValue.SettingsData[0].Values[2].Value); + + Assert.AreEqual("#2: The second content value in Danish", blockListValue.ContentData[1].Values[2].Value); + Assert.AreEqual("#2: The second settings value in Danish", blockListValue.SettingsData[1].Values[2].Value); + }); + + // limited user access means invariant, English and German should not have been updated - changes should be rolled back to the initial block values + if (updateWithLimitedUserAccess) + { + Assert.Multiple(() => + { + + Assert.AreEqual("#1: The first invariant content value", blockListValue.ContentData[0].Values[0].Value); + Assert.AreEqual("#1: The first invariant settings value", blockListValue.SettingsData[0].Values[0].Value); + Assert.AreEqual("#1: The first content value in English", blockListValue.ContentData[0].Values[1].Value); + Assert.AreEqual("#1: The first settings value in English", blockListValue.SettingsData[0].Values[1].Value); + Assert.AreEqual("#1: The first content value in German", blockListValue.ContentData[0].Values[3].Value); + Assert.AreEqual("#1: The first settings value in German", blockListValue.SettingsData[0].Values[3].Value); + + Assert.AreEqual("#2: The first invariant content value", blockListValue.ContentData[1].Values[0].Value); + Assert.AreEqual("#2: The first invariant settings value", blockListValue.SettingsData[1].Values[0].Value); + Assert.AreEqual("#2: The first content value in English", blockListValue.ContentData[1].Values[1].Value); + Assert.AreEqual("#2: The first settings value in English", blockListValue.SettingsData[1].Values[1].Value); + Assert.AreEqual("#2: The first content value in German", blockListValue.ContentData[1].Values[3].Value); + Assert.AreEqual("#2: The first settings value in German", blockListValue.SettingsData[1].Values[3].Value); + }); + } + else + { + Assert.Multiple(() => + { + Assert.AreEqual("#1: The second invariant content value", blockListValue.ContentData[0].Values[0].Value); + Assert.AreEqual("#1: The second invariant settings value", blockListValue.SettingsData[0].Values[0].Value); + Assert.AreEqual("#1: The second content value in English", blockListValue.ContentData[0].Values[1].Value); + Assert.AreEqual("#1: The second settings value in English", blockListValue.SettingsData[0].Values[1].Value); + Assert.AreEqual("#1: The second content value in German", blockListValue.ContentData[0].Values[3].Value); + Assert.AreEqual("#1: The second settings value in German", blockListValue.SettingsData[0].Values[3].Value); + + Assert.AreEqual("#2: The second invariant content value", blockListValue.ContentData[1].Values[0].Value); + Assert.AreEqual("#2: The second invariant settings value", blockListValue.SettingsData[1].Values[0].Value); + Assert.AreEqual("#2: The second content value in English", blockListValue.ContentData[1].Values[1].Value); + Assert.AreEqual("#2: The second settings value in English", blockListValue.SettingsData[1].Values[1].Value); + Assert.AreEqual("#2: The second content value in German", blockListValue.ContentData[1].Values[3].Value); + Assert.AreEqual("#2: The second settings value in German", blockListValue.SettingsData[1].Values[3].Value); + }); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Handle_Limited_User_Access_To_Languages_In_Nested_Blocks_Without_Access_With_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess) + { + await LanguageService.CreateAsync( + new Language("de-DE", "German"), Constants.Security.SuperUserKey); + var userKey = updateWithLimitedUserAccess + ? (await CreateLimitedUser()).Key + : Constants.Security.SuperUserKey; + var nestedElementType = CreateElementType(ContentVariation.Culture); + var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType); + + var rootElementType = new ContentTypeBuilder() + .WithAlias("myRootElementType") + .WithName("My Root Element Type") + .WithIsElement(true) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("nestedBlocks") + .WithName("Nested blocks") + .WithDataTypeId(nestedBlockListDataType.Id) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList) + .WithValueStorageType(ValueStorageType.Ntext) + .WithVariations(ContentVariation.Nothing) + .Done() + .Build(); + ContentTypeService.Save(rootElementType); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var nestedElementContentKey = Guid.NewGuid(); + var nestedElementSettingsKey = Guid.NewGuid(); + var content = CreateContent( + contentType, + rootElementType, + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKey, + nestedElementSettingsKey, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first nested content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "The first nested content value in German", Culture = "de-DE" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first nested settings value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "The first nested settings value in German", Culture = "de-DE" }, + }, + null, + null)) + } + }, + [], + false); + content.SetCultureName("Home (de)", "de-DE"); + ContentService.Save(content); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKey, + nestedElementSettingsKey, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The second nested content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "The second nested content value in German", Culture = "de-DE" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The second nested settings value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "The second nested settings value in German", Culture = "de-DE" }, + }, + null, + null)); + + var updateModel = new ContentUpdateModel + { + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + }, + Variants = new[] + { + new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] }, + new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] }, + new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] } + } + }; + + var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey); + Assert.IsTrue(result.Success); + + content = ContentService.GetById(content.Key); + var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString(); + Assert.NotNull(savedBlocksValue); + blockListValue = JsonSerializer.Deserialize(savedBlocksValue); + + var nestedBlocksPropertyValue = blockListValue.ContentData + .FirstOrDefault()?.Values + .FirstOrDefault(v => v.Alias == "nestedBlocks")?.Value?.ToString(); + Assert.IsNotNull(nestedBlocksPropertyValue); + var nestedBlockListValue = JsonSerializer.Deserialize(nestedBlocksPropertyValue); + + + // the Danish and invariant values should be updated regardless of the executing user + Assert.Multiple(() => + { + Assert.AreEqual("The second nested invariant content value", nestedBlockListValue.ContentData[0].Values[0].Value); + Assert.AreEqual("The second nested content value in Danish", nestedBlockListValue.ContentData[0].Values[2].Value); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockListValue.SettingsData[0].Values[0].Value); + Assert.AreEqual("The second nested settings value in Danish", nestedBlockListValue.SettingsData[0].Values[2].Value); + }); + + // limited user access means English and German should not have been updated - changes should be rolled back to the initial block values + if (updateWithLimitedUserAccess) + { + Assert.Multiple(() => + { + Assert.AreEqual("The first nested content value in English", nestedBlockListValue.ContentData[0].Values[1].Value); + Assert.AreEqual("The first nested content value in German", nestedBlockListValue.ContentData[0].Values[3].Value); + + Assert.AreEqual("The first nested settings value in English", nestedBlockListValue.SettingsData[0].Values[1].Value); + Assert.AreEqual("The first nested settings value in German", nestedBlockListValue.SettingsData[0].Values[3].Value); + }); + } + else + { + Assert.Multiple(() => + { + Assert.AreEqual("The second nested content value in English", nestedBlockListValue.ContentData[0].Values[1].Value); + Assert.AreEqual("The second nested content value in German", nestedBlockListValue.ContentData[0].Values[3].Value); + + Assert.AreEqual("The second nested settings value in English", nestedBlockListValue.SettingsData[0].Values[1].Value); + Assert.AreEqual("The second nested settings value in German", nestedBlockListValue.SettingsData[0].Values[3].Value); + }); + } + } + + private async Task CreateLimitedUser() + { + var userGroupService = GetRequiredService(); + var userService = GetRequiredService(); + + var danish = await LanguageService.GetAsync("da-DK"); + Assert.IsNotNull(danish); + + var user = UserBuilder.CreateUser(); + userService.Save(user); + + var group = UserGroupBuilder.CreateUserGroup(); + group.ClearAllowedLanguages(); + group.AddAllowedLanguage(danish.Id); + + var userGroupResult = await userGroupService.CreateAsync(group, Constants.Security.SuperUserKey, [user.Key]); + Assert.IsTrue(userGroupResult.Success); + + return user; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs index 53af4af967..14fb9c5c72 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs @@ -16,6 +16,12 @@ public partial class BlockListElementLevelVariationTests : BlockEditorElementVar public void OneTimeSetUp() { TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Publish_Invariant_Properties_Without_Default_Culture_With_AllowEditInvariantFromNonDefault)); + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Handle_Limited_User_Access_To_Languages_With_AllowEditInvariantFromNonDefault)); + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Handle_Limited_User_Access_To_Languages_In_Nested_Blocks_Without_Access_With_AllowEditInvariantFromNonDefault)); + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Handle_Limited_User_Access_To_Languages_With_AllowEditInvariantFromNonDefault) + "(True)"); + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Handle_Limited_User_Access_To_Languages_With_AllowEditInvariantFromNonDefault) + "(False)"); + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Handle_Limited_User_Access_To_Languages_In_Nested_Blocks_Without_Access_With_AllowEditInvariantFromNonDefault) + "(True)"); + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Handle_Limited_User_Access_To_Languages_In_Nested_Blocks_Without_Access_With_AllowEditInvariantFromNonDefault) + "(False)"); } private IJsonSerializer JsonSerializer => GetRequiredService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 8f9910e07d..fd536e4438 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -161,6 +161,9 @@ BlockListElementLevelVariationTests.cs + + BlockListElementLevelVariationTests.cs + BlockListElementLevelVariationTests.cs