Media: Add protection to restrict access to media in recycle bin (closes #2931) (#20378)

* Add MoveFile it IFileSystem and implement on file systems.

* Rename media file on move to recycle bin.

* Rename file on restore from recycle bin.

* Add configuration to enabled recycle bin media protection.

* Expose backoffice authentication as cookie for non-backoffice usage.
Protected requests for media in recycle bin.

* Display protected image when viewing image cropper in the backoffice media recycle bin.

* Code tidy and comments.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Introduced helper class to DRY up repeated code between image cropper and file upload notification handlers.

* Reverted client-side and management API updates.

* Moved update of path to media file in recycle bin with deleted suffix to the server.

* Separate integration tests for add and remove.

* Use interpolated strings.

* Renamed variable.

* Move EnableMediaRecycleBinProtection to ContentSettings.

* Tidied up comments.

* Added TODO for 18.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andy Butland
2025-11-04 08:39:44 +01:00
committed by GitHub
parent b502e29d51
commit 2b8146f72d
24 changed files with 757 additions and 34 deletions

View File

@@ -1,7 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Strings;
@@ -40,7 +38,58 @@ public sealed class MediaFileManager
/// Delete media files.
/// </summary>
/// <param name="files">Files to delete (filesystem-relative paths).</param>
public void DeleteMediaFiles(IEnumerable<string> files)
public void DeleteMediaFiles(IEnumerable<string> files) =>
PerformMediaFileOperation(
files,
file =>
{
FileSystem.DeleteFile(file);
var directory = _mediaPathScheme.GetDeleteDirectory(this, file);
if (!directory.IsNullOrWhiteSpace())
{
FileSystem.DeleteDirectory(directory!, true);
}
},
"Failed to delete media file '{File}'.");
/// <summary>
/// Adds a suffix to media files.
/// </summary>
/// <param name="files">Files to append a suffix to.</param>
/// <param name="suffix">The suffix to append.</param>
/// <remarks>
/// The suffix will be added prior to the file extension, e.g. "image.jpg" with suffix ".deleted" will become "image.deleted.jpg".
/// </remarks>
public void SuffixMediaFiles(IEnumerable<string> files, string suffix)
=> PerformMediaFileOperation(
files,
file =>
{
var suffixedFile = Path.ChangeExtension(file, suffix + Path.GetExtension(file));
FileSystem.MoveFile(file, suffixedFile);
},
"Failed to rename media file '{File}'.");
/// <summary>
/// Removes a suffix from media files.
/// </summary>
/// <param name="files">Files to remove a suffix from.</param>
/// <param name="suffix">The suffix to remove.</param>
/// <remarks>
/// The suffix will be removed prior to the file extension, e.g. "image.deleted.jpg" with suffix ".deleted" will become "image.jpg".
/// </remarks>
public void RemoveSuffixFromMediaFiles(IEnumerable<string> files, string suffix)
=> PerformMediaFileOperation(
files,
file =>
{
var fileWithSuffixRemoved = file.Replace(suffix + Path.GetExtension(file), Path.GetExtension(file));
FileSystem.MoveFile(file, fileWithSuffixRemoved);
},
"Failed to rename media file '{File}'.");
private void PerformMediaFileOperation(IEnumerable<string> files, Action<string> fileOperation, string errorMessage)
{
files = files.Distinct();
@@ -61,17 +110,11 @@ public sealed class MediaFileManager
return;
}
FileSystem.DeleteFile(file);
var directory = _mediaPathScheme.GetDeleteDirectory(this, file);
if (!directory.IsNullOrWhiteSpace())
{
FileSystem.DeleteDirectory(directory!, true);
}
fileOperation(file);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to delete media file '{File}'.", file);
_logger.LogError(e, errorMessage, file);
}
});
}