diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/EmptyDocumentRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/EmptyDocumentRecycleBinController.cs new file mode 100644 index 0000000000..6445c71c89 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/EmptyDocumentRecycleBinController.cs @@ -0,0 +1,54 @@ +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.Actions; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin; + +[ApiVersion("1.0")] +public class EmptyDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IContentService _contentService; + + public EmptyDocumentRecycleBinController( + IEntityService entityService, + IAuthorizationService authorizationService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentService contentService) + : base(entityService) + { + _authorizationService = authorizationService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _contentService = contentService; + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task EmptyRecycleBin() + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.RecycleBin(ActionDelete.ActionLetter), + AuthorizationPolicies.ContentPermissionByResource); + + if (authorizationResult.Succeeded is false) + { + return Forbidden(); + } + + OperationResult result = await _contentService.EmptyRecycleBinAsync(CurrentUserKey(_backOfficeSecurityAccessor)); + return result.Success + ? Ok() + : OperationStatusResult(result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/EmptyMediaRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/EmptyMediaRecycleBinController.cs new file mode 100644 index 0000000000..c235c163b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/EmptyMediaRecycleBinController.cs @@ -0,0 +1,54 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Controllers.Media.RecycleBin; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin; + +[ApiVersion("1.0")] +public class EmptyMediaRecycleBinController : MediaRecycleBinControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IMediaService _mediaService; + + public EmptyMediaRecycleBinController( + IEntityService entityService, + IAuthorizationService authorizationService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMediaService mediaService) + : base(entityService) + { + _authorizationService = authorizationService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mediaService = mediaService; + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task EmptyRecycleBin() + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.RecycleBin(), + AuthorizationPolicies.MediaPermissionByResource); + + if (authorizationResult.Succeeded is false) + { + return Forbidden(); + } + + OperationResult result = await _mediaService.EmptyRecycleBinAsync(CurrentUserKey(_backOfficeSecurityAccessor)); + return result.Success + ? Ok() + : OperationStatusResult(result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs index 36459f1e09..be6740b031 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -77,6 +79,18 @@ public abstract class RecycleBinControllerBase : ManagementApiControllerB return viewModel; } + protected IActionResult OperationStatusResult(OperationResult result) => + result.Result switch + { + OperationResultType.FailedCancelledByEvent => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Cancelled by notification") + .WithDetail("A notification handler prevented the operation.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder() + .WithTitle("Unknown operation status.") + .Build()), + }; + private IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) { IEntitySlim[] rootEntities = _entityService diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 5b204be532..7c8f5b06ff 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -5674,6 +5674,50 @@ ] } }, + "/umbraco/management/api/v1/recycle-bin/document": { + "delete": { + "tags": [ + "Document" + ], + "operationId": "DeleteRecycleBinDocument", + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/recycle-bin/document/children": { "get": { "tags": [ @@ -10025,6 +10069,50 @@ ] } }, + "/umbraco/management/api/v1/recycle-bin/media": { + "delete": { + "tags": [ + "Media" + ], + "operationId": "DeleteRecycleBinMedia", + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/recycle-bin/media/children": { "get": { "tags": [ diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj index 37aa74414c..bb9874e5df 100644 --- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj +++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index da0d5d6ad6..3c351a658b 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -32,6 +32,7 @@ public class ContentService : RepositoryService, IContentService private readonly Lazy _propertyValidationService; private readonly IShortStringHelper _shortStringHelper; private readonly ICultureImpactFactory _cultureImpactFactory; + private readonly IUserIdKeyResolver _userIdKeyResolver; private IQuery? _queryNotTrashed; #region Constructors @@ -48,7 +49,8 @@ public class ContentService : RepositoryService, IContentService ILanguageRepository languageRepository, Lazy propertyValidationService, IShortStringHelper shortStringHelper, - ICultureImpactFactory cultureImpactFactory) + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -60,10 +62,11 @@ public class ContentService : RepositoryService, IContentService _propertyValidationService = propertyValidationService; _shortStringHelper = shortStringHelper; _cultureImpactFactory = cultureImpactFactory; + _userIdKeyResolver = userIdKeyResolver; _logger = loggerFactory.CreateLogger(); } - [Obsolete("Use constructor that takes ICultureImpactService as a parameter, scheduled for removal in V12")] + [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] public ContentService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -75,7 +78,8 @@ public class ContentService : RepositoryService, IContentService IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository, Lazy propertyValidationService, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory) : this( provider, loggerFactory, @@ -88,7 +92,8 @@ public class ContentService : RepositoryService, IContentService languageRepository, propertyValidationService, shortStringHelper, - StaticServiceProvider.Instance.GetRequiredService()) + cultureImpactFactory, + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -2520,6 +2525,9 @@ public class ContentService : RepositoryService, IContentService _documentRepository.Save(content); } + public async Task EmptyRecycleBinAsync(Guid userId) + => EmptyRecycleBin(await _userIdKeyResolver.GetAsync(userId)); + /// /// Empties the Recycle Bin by deleting all that resides in the bin /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 9ca9940a71..577e5789f7 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -165,7 +165,7 @@ public interface IContentService : IContentServiceBase /// /// An Enumerable list of objects /// - /// The content returned from this method may be culture variant, in which case you can use + /// The content returned from this method may be culture variant, in which case you can use /// to get the status for a specific culture. /// IEnumerable GetContentForExpiration(DateTime date); @@ -175,7 +175,7 @@ public interface IContentService : IContentServiceBase /// /// An Enumerable list of objects /// - /// The content returned from this method may be culture variant, in which case you can use + /// The content returned from this method may be culture variant, in which case you can use /// to get the status for a specific culture. /// IEnumerable GetContentForRelease(DateTime date); @@ -521,4 +521,6 @@ public interface IContentService : IContentServiceBase IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId); #endregion + + Task EmptyRecycleBinAsync(Guid userId); } diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 1e433814f7..a48b717305 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -381,4 +381,6 @@ public interface IMediaService : IContentServiceBase /// The filesystem path to the media. /// The size of the media. long GetMediaFileSize(string filepath); + + Task EmptyRecycleBinAsync(Guid userId); } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 7aba44d16b..63aace15d4 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -1,5 +1,7 @@ using System.Globalization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -25,6 +27,7 @@ namespace Umbraco.Cms.Core.Services private readonly IAuditRepository _auditRepository; private readonly IEntityRepository _entityRepository; private readonly IShortStringHelper _shortStringHelper; + private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly MediaFileManager _mediaFileManager; @@ -39,7 +42,8 @@ namespace Umbraco.Cms.Core.Services IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository, IEntityRepository entityRepository, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + IUserIdKeyResolver userIdKeyResolver) : base(provider, loggerFactory, eventMessagesFactory) { _mediaFileManager = mediaFileManager; @@ -48,6 +52,33 @@ namespace Umbraco.Cms.Core.Services _mediaTypeRepository = mediaTypeRepository; _entityRepository = entityRepository; _shortStringHelper = shortStringHelper; + _userIdKeyResolver = userIdKeyResolver; + } + + [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] + public MediaService( + ICoreScopeProvider provider, + MediaFileManager mediaFileManager, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IMediaRepository mediaRepository, + IAuditRepository auditRepository, + IMediaTypeRepository mediaTypeRepository, + IEntityRepository entityRepository, + IShortStringHelper shortStringHelper) + : this( + provider, + mediaFileManager, + loggerFactory, + eventMessagesFactory, + mediaRepository, + auditRepository, + mediaTypeRepository, + entityRepository, + shortStringHelper, + StaticServiceProvider.Instance.GetRequiredService() + ) + { } #endregion @@ -1089,6 +1120,9 @@ namespace Umbraco.Cms.Core.Services _mediaRepository.Save(media); } + public async Task EmptyRecycleBinAsync(Guid userId) + => EmptyRecycleBin(await _userIdKeyResolver.GetAsync(userId)); + /// /// Empties the Recycle Bin by deleting all that resides in the bin ///