Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add management api delete document endpoints #15600

Merged
merged 5 commits into from Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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;
Expand All @@ -9,6 +10,7 @@
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;
Expand Down
Expand Up @@ -4,11 +4,12 @@
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;
Expand Down
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="JsonPatch.Net" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>

<ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion src/Umbraco.Core/Services/ContentEditingService.cs
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;

Check notice on line 1 in src/Umbraco.Core/Services/ContentEditingService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

ℹ Getting worse: Primitive Obsession

The ratio of primitive types in function arguments increases from 61.82% to 63.16%, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.PropertyEditors;
Expand Down Expand Up @@ -80,8 +80,11 @@
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);
Expand Down
30 changes: 23 additions & 7 deletions src/Umbraco.Core/Services/ContentEditingServiceBase.cs
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;

Check notice on line 1 in src/Umbraco.Core/Services/ContentEditingServiceBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

ℹ Getting worse: Overall Code Complexity

The mean cyclomatic complexity increases from 4.05 to 4.26, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.

Check notice on line 1 in src/Umbraco.Core/Services/ContentEditingServiceBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

✅ Getting better: Primitive Obsession

The ratio of primitive types in function arguments decreases from 41.82% to 41.07%, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
Expand Down Expand Up @@ -103,13 +103,17 @@
}

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);
Expand All @@ -118,9 +122,11 @@
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))

Check warning on line 127 in src/Umbraco.Core/Services/ContentEditingServiceBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

❌ New issue: Complex Conditional

HandleDeletionAsync has 1 complex conditionals with 3 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
{
ContentEditingOperationStatus status = mustBeTrashed
ContentEditingOperationStatus status = trashStatusRequirement is ContentTrashStatusRequirement.MustBeTrashed
? ContentEditingOperationStatus.NotInTrash
: ContentEditingOperationStatus.InTrash;
return await Task.FromResult(Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(status, content));
Expand Down Expand Up @@ -470,4 +476,14 @@

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
}
}
10 changes: 9 additions & 1 deletion src/Umbraco.Core/Services/IContentEditingService.cs
Expand Up @@ -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);
}
Expand Up @@ -8,12 +8,12 @@ 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.DeleteAsync(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);

Expand All @@ -22,26 +22,26 @@ public async Task Can_Delete(bool variant)
Assert.IsNull(content);
}

[Test]
public async Task Cannot_Delete_Non_Existing()
{
var result = await ContentEditingService.DeleteAsync(Guid.NewGuid(), Constants.Security.SuperUserKey);
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)
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.IsFalse(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.NotInTrash, result.Status);
Assert.IsTrue(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);

// re-get and verify that deletion did not happen
// re-get and verify deletion
content = await ContentEditingService.GetAsync(content.Key);
Assert.IsNotNull(content);
Assert.IsNull(content);
}

[Test]
public async Task Cannot_Delete_Non_Existing()
{
var result = await ContentEditingService.DeleteAsync(Guid.NewGuid(), Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status);
}
}
@@ -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);
}
}
Expand Up @@ -25,7 +25,7 @@ public async Task Can_Move_To_Recycle_Bin(bool variant)
[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);
}
Expand Down
Expand Up @@ -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">
Expand Down Expand Up @@ -115,6 +115,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>
Expand Down