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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user