Add management api delete document endpoints (#15600)
* Added Delete document endpoint * Added delete from recylebin endpoint Added/restructured unittests * Clarifications * Moved document specific mapping from base recyclebin to document recyclebin * Refactor OperationStatusResult mapping trough inheritance --------- Co-authored-by: Sven Geusens <sge@umbraco.dk>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public DeleteDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionDelete.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<IContent?, ContentEditingOperationStatus> result = await _contentEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: ContentEditingOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteDocumentRecycleBinController : DocumentRecycleBinControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
|
||||
public DeleteDocumentRecycleBinController(
|
||||
IEntityService entityService,
|
||||
IAuthorizationService authorizationService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IContentEditingService contentEditingService)
|
||||
: base(entityService)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_contentEditingService = contentEditingService;
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionDelete.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<IContent?, ContentEditingOperationStatus> result = await _contentEditingService.DeleteFromRecycleBinAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: ContentEditingOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
@@ -9,6 +10,7 @@ using Umbraco.Cms.Api.Management.Controllers.RecycleBin;
|
||||
using Umbraco.Cms.Api.Management.Filters;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.RecycleBin;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
|
||||
|
||||
@@ -6,11 +6,12 @@ using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.Services.Paging;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Content;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.RecycleBin;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.RecycleBin;
|
||||
|
||||
public abstract class RecycleBinControllerBase<TItem> : ManagementApiControllerBase
|
||||
public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
|
||||
where TItem : RecycleBinItemResponseModel, new()
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
|
||||
@@ -80,8 +80,11 @@ internal sealed class ContentEditingService
|
||||
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveToRecycleBinAsync(Guid key, Guid userKey)
|
||||
=> await HandleMoveToRecycleBinAsync(key, userKey);
|
||||
|
||||
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteFromRecycleBinAsync(Guid key, Guid userKey)
|
||||
=> await HandleDeleteAsync(key, userKey, true);
|
||||
|
||||
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey)
|
||||
=> await HandleDeleteAsync(key, userKey);
|
||||
=> await HandleDeleteAsync(key, userKey, false);
|
||||
|
||||
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveAsync(Guid key, Guid? parentKey, Guid userKey)
|
||||
=> await HandleMoveAsync(key, parentKey, userKey);
|
||||
|
||||
@@ -103,13 +103,17 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
}
|
||||
|
||||
protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleMoveToRecycleBinAsync(Guid key, Guid userKey)
|
||||
=> await HandleDeletionAsync(key, userKey, false, MoveToRecycleBin);
|
||||
=> await HandleDeletionAsync(key, userKey, ContentTrashStatusRequirement.MustNotBeTrashed, MoveToRecycleBin);
|
||||
|
||||
protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeleteAsync(Guid key, Guid userKey)
|
||||
=> await HandleDeletionAsync(key, userKey, true, Delete);
|
||||
protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeleteAsync(Guid key, Guid userKey, bool mustBeTrashed = true)
|
||||
=> await HandleDeletionAsync(key, userKey, mustBeTrashed ? ContentTrashStatusRequirement.MustBeTrashed : ContentTrashStatusRequirement.Irrelevant, Delete);
|
||||
|
||||
// helper method to perform move-to-recycle-bin and delete for content as they are very much handled in the same way
|
||||
private async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeletionAsync(Guid key, Guid userKey, bool mustBeTrashed, Func<TContent, int, OperationResult?> performDelete)
|
||||
// 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)
|
||||
{
|
||||
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
|
||||
TContent? content = ContentService.GetById(key);
|
||||
@@ -118,9 +122,11 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
return await Task.FromResult(Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, content));
|
||||
}
|
||||
|
||||
if (content.Trashed != mustBeTrashed)
|
||||
// checking the trash status is not done when it is irrelevant
|
||||
if ((trashStatusRequirement is ContentTrashStatusRequirement.MustBeTrashed && content.Trashed is false)
|
||||
|| (trashStatusRequirement is ContentTrashStatusRequirement.MustNotBeTrashed && content.Trashed is true))
|
||||
{
|
||||
ContentEditingOperationStatus status = mustBeTrashed
|
||||
ContentEditingOperationStatus status = trashStatusRequirement is ContentTrashStatusRequirement.MustBeTrashed
|
||||
? ContentEditingOperationStatus.NotInTrash
|
||||
: ContentEditingOperationStatus.InTrash;
|
||||
return await Task.FromResult(Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(status, content));
|
||||
@@ -470,4 +476,14 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
|
||||
|
||||
private static Dictionary<string, IPropertyType> GetPropertyTypesByAlias(TContentType contentType)
|
||||
=> contentType.CompositionPropertyTypes.ToDictionary(pt => pt.Alias);
|
||||
|
||||
/// <summary>
|
||||
/// Should never be made public, serves the purpose of a nullable bool but more readable.
|
||||
/// </summary>
|
||||
private enum ContentTrashStatusRequirement
|
||||
{
|
||||
Irrelevant,
|
||||
MustBeTrashed,
|
||||
MustNotBeTrashed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,19 @@ public interface IContentEditingService
|
||||
|
||||
Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveToRecycleBinAsync(Guid key, Guid userKey);
|
||||
|
||||
Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey);
|
||||
/// <summary>
|
||||
/// Deletes a Content Item if it is in the recycle bin.
|
||||
/// </summary>
|
||||
Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteFromRecycleBinAsync(Guid key, Guid userKey);
|
||||
|
||||
Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveAsync(Guid key, Guid? parentKey, Guid userKey);
|
||||
|
||||
Task<Attempt<IContent?, ContentEditingOperationStatus>> CopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey);
|
||||
|
||||
Task<ContentEditingOperationStatus> SortAsync(Guid? parentKey, IEnumerable<SortingModel> sortingModels, Guid userKey);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a Content Item whether it is in the recycle bin or not.
|
||||
/// </summary>
|
||||
Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,26 @@ public partial class ContentEditingServiceTests
|
||||
{
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Delete(bool variant)
|
||||
public async Task Can_Delete_FromRecycleBin(bool variant)
|
||||
{
|
||||
var content = await (variant ? CreateVariantContent() : CreateInvariantContent());
|
||||
await ContentEditingService.MoveToRecycleBinAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
|
||||
var result = await ContentEditingService.DeleteFromRecycleBinAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
|
||||
|
||||
// re-get and verify deletion
|
||||
content = await ContentEditingService.GetAsync(content.Key);
|
||||
Assert.IsNull(content);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Delete_FromOutsideOfRecycleBin(bool variant)
|
||||
{
|
||||
var content = await (variant ? CreateVariantContent() : CreateInvariantContent());
|
||||
|
||||
var result = await ContentEditingService.DeleteAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
|
||||
@@ -29,19 +44,4 @@ public partial class ContentEditingServiceTests
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Cannot_Delete_If_Not_In_Recycle_Bin(bool variant)
|
||||
{
|
||||
var content = await (variant ? CreateVariantContent() : CreateInvariantContent());
|
||||
|
||||
var result = await ContentEditingService.DeleteAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotInTrash, result.Status);
|
||||
|
||||
// re-get and verify that deletion did not happen
|
||||
content = await ContentEditingService.GetAsync(content.Key);
|
||||
Assert.IsNotNull(content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
|
||||
|
||||
public partial class ContentEditingServiceTests
|
||||
{
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_DeleteFromRecycleBin_If_InsideRecycleBin(bool variant)
|
||||
{
|
||||
var content = await (variant ? CreateVariantContent() : CreateInvariantContent());
|
||||
await ContentEditingService.MoveToRecycleBinAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
|
||||
var result = await ContentEditingService.DeleteFromRecycleBinAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
|
||||
|
||||
// re-get and verify deletion
|
||||
content = await ContentEditingService.GetAsync(content.Key);
|
||||
Assert.IsNull(content);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Delete_FromRecycleBin_Non_Existing()
|
||||
{
|
||||
var result = await ContentEditingService.DeleteFromRecycleBinAsync(Guid.NewGuid(), Constants.Security.SuperUserKey);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Cannot_Delete_FromRecycleBin_If_Not_In_Recycle_Bin(bool variant)
|
||||
{
|
||||
var content = await (variant ? CreateVariantContent() : CreateInvariantContent());
|
||||
|
||||
var result = await ContentEditingService.DeleteFromRecycleBinAsync(content.Key, Constants.Security.SuperUserKey);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotInTrash, result.Status);
|
||||
|
||||
// re-get and verify that deletion did not happen
|
||||
content = await ContentEditingService.GetAsync(content.Key);
|
||||
Assert.IsNotNull(content);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public partial class ContentEditingServiceTests
|
||||
[Test]
|
||||
public async Task Cannot_Move_Non_Existing_To_Recycle_Bin()
|
||||
{
|
||||
var result = await ContentEditingService.DeleteAsync(Guid.NewGuid(), Constants.Security.SuperUserKey);
|
||||
var result = await ContentEditingService.MoveToRecycleBinAsync(Guid.NewGuid(), Constants.Security.SuperUserKey);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<Compile Update="Umbraco.Infrastructure\Services\ContentEditingServiceTests.Copy.cs">
|
||||
<DependentUpon>ContentEditingServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Infrastructure\Services\ContentEditingServiceTests.Delete.cs">
|
||||
<Compile Update="Umbraco.Infrastructure\Services\ContentEditingServiceTests.DeleteFromRecycleBin.cs">
|
||||
<DependentUpon>ContentEditingServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Infrastructure\Services\ContentEditingServiceTests.Get.cs">
|
||||
@@ -121,6 +121,9 @@
|
||||
<Compile Update="Umbraco.Core\Services\MediaTypeEditingServiceTests.Update.cs">
|
||||
<DependentUpon>MediaTypeEditingServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Infrastructure\Services\ContentEditingServiceTests.MoveToRecyleBin.cs">
|
||||
<DependentUpon>ContentEditingServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user