V14/feature/delete media in recyblebin (#15636)

* Bugfix: MediaCacheRefresher needs to always clear the mediaCache no matter what the publishedState is

* fix: check correct permissions for deleteDocumentFromRecycleBin

* Fix: ImageCropper propertyValues should not hold invalid values.

* Added media delete endpoints

* PR comment fix: Do not schedule cleanup if we know the file does not exist.

* resolved forward merge build conflicts

namespace cleanup

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
This commit is contained in:
Sven Geusens
2024-02-07 10:44:20 +01:00
committed by GitHub
parent 9d07268ead
commit 024bb8903c
8 changed files with 163 additions and 36 deletions

View File

@@ -44,7 +44,7 @@ public class DeleteDocumentRecycleBinController : DocumentRecycleBinControllerBa
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionDelete.ActionLetter, id),
ContentPermissionResource.RecycleBin(ActionDelete.ActionLetter),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)

View File

@@ -0,0 +1,56 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Security.Authorization.Media;
using Umbraco.Cms.Core;
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.Media;
[ApiVersion("1.0")]
public class DeleteMediaController : MediaControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IMediaEditingService _mediaEditingService;
public DeleteMediaController(
IAuthorizationService authorizationService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMediaEditingService mediaEditingService)
{
_authorizationService = authorizationService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_mediaEditingService = mediaEditingService;
}
[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,
MediaPermissionResource.RecycleBin(),
AuthorizationPolicies.MediaPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
Attempt<IMedia?, ContentEditingOperationStatus> result = await _mediaEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,60 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Security.Authorization.Media;
using Umbraco.Cms.Core;
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.Media.RecycleBin;
[ApiVersion("1.0")]
public class DeleteMediaRecycleBinController : MediaRecycleBinControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IMediaEditingService _mediaEditingService;
public DeleteMediaRecycleBinController(
IEntityService entityService,
IAuthorizationService authorizationService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMediaEditingService mediaEditingService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService,mediaPresentationFactory)
{
_authorizationService = authorizationService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_mediaEditingService = mediaEditingService;
}
[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,
MediaPermissionResource.WithKeys(id),
AuthorizationPolicies.MediaPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
Attempt<IMedia?, ContentEditingOperationStatus> result = await _mediaEditingService.DeleteFromRecycleBinAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}

View File

@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
@@ -8,7 +7,6 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Controllers.RecycleBin;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Filters;
using Umbraco.Cms.Api.Management.ViewModels.RecycleBin;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Media.RecycleBin;
using Umbraco.Cms.Web.Common.Authorization;

View File

@@ -73,40 +73,41 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
return;
}
_publishedSnapshotService.Notify(payloads, out var anythingChanged);
// actions that always need to happen
AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
Attempt<IAppPolicyCache?> mediaCache = AppCaches.IsolatedCaches.Get<IMedia>();
if (anythingChanged)
foreach (JsonPayload payload in payloads)
{
if (payload.ChangeTypes == TreeChangeTypes.Remove)
{
_idKeyMap.ClearCache(payload.Id);
}
if (!mediaCache.Success)
{
continue;
}
// repository cache
// it *was* done for each pathId but really that does not make sense
// only need to do it for the current media
mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMedia, int>(payload.Id));
mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMedia, Guid?>(payload.Key));
// remove those that are in the branch
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
{
var pathid = "," + payload.Id + ",";
mediaCache.Result?.ClearOfType<IMedia>((_, v) => v.Path?.Contains(pathid) ?? false);
}
}
_publishedSnapshotService.Notify(payloads, out var hasPublishedDataChanged);
// we only need to clear this if the published cache has changed
if (hasPublishedDataChanged)
{
AppCaches.ClearPartialViewCache();
AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
Attempt<IAppPolicyCache?> mediaCache = AppCaches.IsolatedCaches.Get<IMedia>();
foreach (JsonPayload payload in payloads)
{
if (payload.ChangeTypes == TreeChangeTypes.Remove)
{
_idKeyMap.ClearCache(payload.Id);
}
if (!mediaCache.Success)
{
continue;
}
// repository cache
// it *was* done for each pathId but really that does not make sense
// only need to do it for the current media
mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMedia, int>(payload.Id));
mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMedia, Guid?>(payload.Key));
// remove those that are in the branch
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
{
var pathid = "," + payload.Id + ",";
mediaCache.Result?.ClearOfType<IMedia>((_, v) => v.Path?.Contains(pathid) ?? false);
}
}
}
base.Refresh(payloads);

View File

@@ -24,4 +24,5 @@ public interface IMediaEditingService
Task<Attempt<IMedia?, ContentEditingOperationStatus>> MoveAsync(Guid key, Guid? parentKey, Guid userKey);
Task<ContentEditingOperationStatus> SortAsync(Guid? parentKey, IEnumerable<SortingModel> sortingModels, Guid userKey);
Task<Attempt<IMedia?, ContentEditingOperationStatus>> DeleteFromRecycleBinAsync(Guid key, Guid userKey);
}

View File

@@ -84,7 +84,10 @@ internal sealed class MediaEditingService
=> await HandleMoveToRecycleBinAsync(key, userKey);
public async Task<Attempt<IMedia?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey)
=> await HandleDeleteAsync(key, userKey);
=> await HandleDeleteAsync(key, userKey,false);
public async Task<Attempt<IMedia?, ContentEditingOperationStatus>> DeleteFromRecycleBinAsync(Guid key, Guid userKey)
=> await HandleDeleteAsync(key, userKey, true);
public async Task<Attempt<IMedia?, ContentEditingOperationStatus>> MoveAsync(Guid key, Guid? parentKey, Guid userKey)
=> await HandleMoveAsync(key, parentKey, userKey);

View File

@@ -152,7 +152,10 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
if (temporaryFileKey.HasValue)
{
file = TryGetTemporaryFile(temporaryFileKey.Value);
_temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey.Value, _scopeProvider);
if (file is not null)
{
_temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey.Value, _scopeProvider);
}
}
if (file == null) // not uploading a file
@@ -166,6 +169,11 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
return null; // clear
}
if (editorImageCropperValue is not null && temporaryFileKey.HasValue)
{
// a plausible tempFile value was supplied, but could not be converted to an actual file => clear the src
editorImageCropperValue.Src = null;
}
return _jsonSerializer.Serialize(editorImageCropperValue); // unchanged
}