Skip to content

Commit

Permalink
V15: Dont delete when referenced setting is enabled (#18359)
Browse files Browse the repository at this point in the history
* Take content settings into account

* Implement test

* Amend error message

* Move new enums to have same values

* Update to check in ServiceBase

* Fix empty recycle bin

* Return proper operation status

* Apply suggestions from code review

Co-authored-by: Kenn Jacobsen <[email protected]>

* Fix according to feedback

---------

Co-authored-by: Kenn Jacobsen <[email protected]>
  • Loading branch information
Zeegaan and kjac authored Feb 21, 2025
1 parent 21a0716 commit 368f6eb
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperat
.WithTitle("Duplicate name")
.WithDetail("The supplied name is already in use for the same content type.")
.Build()),
ContentEditingOperationStatus.CannotDeleteWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle("Cannot delete a referenced content item")
.WithDetail("Cannot delete a referenced document, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
.Build()),
ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle("Cannot move a referenced document to the recycle bin")
.WithDetail("Cannot move a referenced document to the recycle bin, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
.Build()),
ContentEditingOperationStatus.Unknown => StatusCode(
StatusCodes.Status500InternalServerError,
problemDetailsBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ protected IActionResult DocumentPublishingOperationStatusResult(
.WithDetail(
"Cannot handle an unpublish time that is not after the current server time.")
.Build()),
ContentPublishingOperationStatus.CannotUnpublishWhenReferenced => BadRequest(problemDetailsBuilder
.WithTitle("Cannot unpublish document when it's referenced somewhere else.")
.WithDetail(
"Cannot unpublish a referenced document, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
.Build()),
ContentPublishingOperationStatus.FailedBranch => BadRequest(problemDetailsBuilder
.WithTitle("Failed branch operation")
.WithDetail("One or more items in the branch could not complete the operation.")
Expand Down
8 changes: 6 additions & 2 deletions src/Umbraco.Core/Services/ContentBlueprintEditingService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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.PropertyEditors;
Expand All @@ -21,8 +23,10 @@ public ContentBlueprintEditingService(
ICoreScopeProvider scopeProvider,
IUserIdKeyResolver userIdKeyResolver,
IContentValidationService validationService,
IContentBlueprintContainerService containerService)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, validationService)
IContentBlueprintContainerService containerService,
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, validationService, optionsMonitor, relationService)
=> _containerService = containerService;

public async Task<IContent?> GetAsync(Guid key)
Expand Down
32 changes: 20 additions & 12 deletions src/Umbraco.Core/Services/ContentEditingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ internal sealed class ContentEditingService
private readonly IUserService _userService;
private readonly ILocalizationService _localizationService;
private readonly ILanguageService _languageService;
private readonly ContentSettings _contentSettings;

public ContentEditingService(
IContentService contentService,
Expand All @@ -36,16 +35,27 @@ public ContentEditingService(
IUserService userService,
ILocalizationService localizationService,
ILanguageService languageService,
IOptions<ContentSettings> contentSettings)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService)
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService)
: base(
contentService,
contentTypeService,
propertyEditorCollection,
dataTypeService,
logger,
scopeProvider,
userIdKeyResolver,
contentValidationService,
treeEntitySortingService,
optionsMonitor,
relationService)
{
_propertyEditorCollection = propertyEditorCollection;
_templateService = templateService;
_logger = logger;
_userService = userService;
_localizationService = localizationService;
_languageService = languageService;
_contentSettings = contentSettings.Value;
}

public async Task<IContent?> GetAsync(Guid key)
Expand Down Expand Up @@ -169,7 +179,7 @@ private async Task<IContent> EnsureOnlyAllowedFieldsAreUpdated(IContent contentW
}

// If property does not support merging, we still need to overwrite if we are not allowed to edit invariant properties.
if (_contentSettings.AllowEditInvariantFromNonDefault is false && allowedToEditDefaultLanguage is false)
if (ContentSettings.AllowEditInvariantFromNonDefault is false && allowedToEditDefaultLanguage is false)
{
foreach (IProperty property in invariantProperties)
{
Expand All @@ -192,7 +202,7 @@ private async Task<IContent> EnsureOnlyAllowedFieldsAreUpdated(IContent contentW
var mergedValue = propertyWithEditor.DataEditor.MergeVariantInvariantPropertyValue(
currentValue,
editedValue,
_contentSettings.AllowEditInvariantFromNonDefault || (defaultLanguage is not null && allowedCultures.Contains(defaultLanguage.IsoCode)),
ContentSettings.AllowEditInvariantFromNonDefault || (defaultLanguage is not null && allowedCultures.Contains(defaultLanguage.IsoCode)),
allowedCultures);

propertyWithEditor.Property.SetValue(mergedValue, null, null);
Expand Down Expand Up @@ -243,10 +253,10 @@ public async Task<Attempt<ContentUpdateResult, ContentEditingOperationStatus>> U
=> await HandleMoveToRecycleBinAsync(key, userKey);

public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteFromRecycleBinAsync(Guid key, Guid userKey)
=> await HandleDeleteAsync(key, userKey, true);
=> await HandleDeleteAsync(key, userKey,true);

public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey)
=> await HandleDeleteAsync(key, userKey, false);
=> await HandleDeleteAsync(key, userKey,false);

public async Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveAsync(Guid key, Guid? parentKey, Guid userKey)
=> await HandleMoveAsync(key, parentKey, userKey);
Expand Down Expand Up @@ -303,11 +313,9 @@ protected override IContent New(string? name, int parentId, IContentType content
protected override IContent? Copy(IContent content, int newParentId, bool relateToOriginal, bool includeDescendants, int userId)
=> ContentService.Copy(content, newParentId, relateToOriginal, includeDescendants, userId);

protected override OperationResult? MoveToRecycleBin(IContent content, int userId)
=> ContentService.MoveToRecycleBin(content, userId);
protected override OperationResult? MoveToRecycleBin(IContent content, int userId) => ContentService.MoveToRecycleBin(content, userId);

protected override OperationResult? Delete(IContent content, int userId)
=> ContentService.Delete(content, userId);
protected override OperationResult? Delete(IContent content, int userId) => ContentService.Delete(content, userId);

protected override IEnumerable<IContent> GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total)
=> ContentService.GetPagedChildren(parentId, pageIndex, pageSize, out total);
Expand Down
48 changes: 43 additions & 5 deletions src/Umbraco.Core/Services/ContentEditingServiceBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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.Editors;
Expand All @@ -20,6 +22,7 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
private readonly ILogger<ContentEditingServiceBase<TContent, TContentType, TContentService, TContentTypeService>> _logger;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IContentValidationServiceBase<TContentType> _validationService;
private readonly IRelationService _relationService;

protected ContentEditingServiceBase(
TContentService contentService,
Expand All @@ -29,13 +32,22 @@ protected ContentEditingServiceBase(
ILogger<ContentEditingServiceBase<TContent, TContentType, TContentService, TContentTypeService>> logger,
ICoreScopeProvider scopeProvider,
IUserIdKeyResolver userIdKeyResolver,
IContentValidationServiceBase<TContentType> validationService)
IContentValidationServiceBase<TContentType> validationService,
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService)
{
_propertyEditorCollection = propertyEditorCollection;
_dataTypeService = dataTypeService;
_logger = logger;
_userIdKeyResolver = userIdKeyResolver;
_validationService = validationService;
ContentSettings = optionsMonitor.CurrentValue;
optionsMonitor.OnChange((contentSettings) =>
{
ContentSettings = contentSettings;
});

_relationService = relationService;
CoreScopeProvider = scopeProvider;
ContentService = contentService;
ContentTypeService = contentTypeService;
Expand All @@ -51,6 +63,8 @@ protected ContentEditingServiceBase(

protected abstract OperationResult? Delete(TContent content, int userId);

protected ContentSettings ContentSettings { get; private set; }

protected ICoreScopeProvider CoreScopeProvider { get; }

protected TContentService ContentService { get; }
Expand Down Expand Up @@ -137,17 +151,35 @@ private async Task<Attempt<ContentValidationResult, ContentEditingOperationStatu
}

protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleMoveToRecycleBinAsync(Guid key, Guid userKey)
=> await HandleDeletionAsync(key, userKey, ContentTrashStatusRequirement.MustNotBeTrashed, MoveToRecycleBin);
=> await HandleDeletionAsync(key,
userKey,
ContentTrashStatusRequirement.MustNotBeTrashed,
MoveToRecycleBin,
ContentSettings.DisableUnpublishWhenReferenced,
ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced);

protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeleteAsync(Guid key, Guid userKey, bool mustBeTrashed = true)
=> await HandleDeletionAsync(key, userKey, mustBeTrashed ? ContentTrashStatusRequirement.MustBeTrashed : ContentTrashStatusRequirement.Irrelevant, Delete);
=> await HandleDeletionAsync(key,
userKey,
mustBeTrashed
? ContentTrashStatusRequirement.MustBeTrashed
: ContentTrashStatusRequirement.Irrelevant,
Delete,
ContentSettings.DisableDeleteWhenReferenced,
ContentEditingOperationStatus.CannotDeleteWhenReferenced);

// helper method to perform move-to-recycle-bin, delete-from-recycle-bin and delete for content as they are very much handled in the same way
// IContentEditingService methods hitting this (ContentTrashStatusRequirement, calledFunction):
// DeleteAsync (irrelevant, Delete)
// MoveToRecycleBinAsync (MustNotBeTrashed, MoveToRecycleBin)
// DeleteFromRecycleBinAsync (MustBeTrashed, Delete)
private async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeletionAsync(Guid key, Guid userKey, ContentTrashStatusRequirement trashStatusRequirement, Func<TContent, int, OperationResult?> performDelete)
private async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeletionAsync(
Guid key,
Guid userKey,
ContentTrashStatusRequirement trashStatusRequirement,
Func<TContent, int, OperationResult?> performDelete,
bool disabledWhenReferenced,
ContentEditingOperationStatus referenceFailStatus)
{
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
TContent? content = ContentService.GetById(key);
Expand All @@ -166,6 +198,11 @@ private async Task<Attempt<ContentValidationResult, ContentEditingOperationStatu
return await Task.FromResult(Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(status, content));
}

if (disabledWhenReferenced && _relationService.IsRelated(content.Id))
{
return Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(referenceFailStatus, content);
}

var userId = await GetUserIdAsync(userKey);
OperationResult? deleteResult = performDelete(content, userId);

Expand Down Expand Up @@ -264,6 +301,7 @@ protected ContentEditingOperationStatus OperationResultToOperationStatus(Operati
// these are the only result states currently expected from the invoked IContentService operations
OperationResultType.Success => ContentEditingOperationStatus.Success,
OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification,
OperationResultType.FailedCannot => ContentEditingOperationStatus.CannotDeleteWhenReferenced,

// for any other state we'll return "unknown" so we know that we need to amend this switch statement
_ => ContentEditingOperationStatus.Unknown
Expand Down Expand Up @@ -479,7 +517,7 @@ private static Dictionary<string, IPropertyType> GetPropertyTypesByAlias(TConten
/// <summary>
/// Should never be made public, serves the purpose of a nullable bool but more readable.
/// </summary>
private enum ContentTrashStatusRequirement
protected internal enum ContentTrashStatusRequirement
{
Irrelevant,
MustBeTrashed,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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.PropertyEditors;
Expand Down Expand Up @@ -26,7 +28,9 @@ protected ContentEditingServiceWithSortingBase(
ICoreScopeProvider scopeProvider,
IUserIdKeyResolver userIdKeyResolver,
IContentValidationServiceBase<TContentType> validationService,
ITreeEntitySortingService treeEntitySortingService)
ITreeEntitySortingService treeEntitySortingService,
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService)
: base(
contentService,
contentTypeService,
Expand All @@ -35,7 +39,9 @@ protected ContentEditingServiceWithSortingBase(
logger,
scopeProvider,
userIdKeyResolver,
validationService)
validationService,
optionsMonitor,
relationService)
{
_logger = logger;
_treeEntitySortingService = treeEntitySortingService;
Expand Down
20 changes: 19 additions & 1 deletion src/Umbraco.Core/Services/ContentPublishingService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
Expand All @@ -16,21 +18,31 @@ internal sealed class ContentPublishingService : IContentPublishingService
private readonly IContentValidationService _contentValidationService;
private readonly IContentTypeService _contentTypeService;
private readonly ILanguageService _languageService;
private ContentSettings _contentSettings;
private readonly IRelationService _relationService;

public ContentPublishingService(
ICoreScopeProvider coreScopeProvider,
IContentService contentService,
IUserIdKeyResolver userIdKeyResolver,
IContentValidationService contentValidationService,
IContentTypeService contentTypeService,
ILanguageService languageService)
ILanguageService languageService,
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService)
{
_coreScopeProvider = coreScopeProvider;
_contentService = contentService;
_userIdKeyResolver = userIdKeyResolver;
_contentValidationService = contentValidationService;
_contentTypeService = contentTypeService;
_languageService = languageService;
_relationService = relationService;
_contentSettings = optionsMonitor.CurrentValue;
optionsMonitor.OnChange((contentSettings) =>
{
_contentSettings = contentSettings;
});
}

/// <inheritdoc />
Expand Down Expand Up @@ -299,6 +311,12 @@ public async Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid
return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound);
}

if (_contentSettings.DisableUnpublishWhenReferenced && _relationService.IsRelated(content.Id))
{
scope.Complete();
return Attempt<ContentPublishingOperationStatus>.Fail(ContentPublishingOperationStatus.CannotUnpublishWhenReferenced);
}

var userId = await _userIdKeyResolver.GetAsync(userKey);

// If cultures are provided for non variant content, and they include the default culture, consider
Expand Down
Loading

0 comments on commit 368f6eb

Please sign in to comment.