Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.TemporaryFile;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
Expand All @@ -21,6 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
internal class FileUploadPropertyValueEditor : DataValueEditor
{
private readonly MediaFileManager _mediaFileManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly ITemporaryFileService _temporaryFileService;
private readonly IScopeProvider _scopeProvider;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
Expand All @@ -40,6 +43,7 @@ public FileUploadPropertyValueEditor(
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
_jsonSerializer = jsonSerializer;
_temporaryFileService = temporaryFileService;
_scopeProvider = scopeProvider;
_fileStreamSecurityValidator = fileStreamSecurityValidator;
Expand All @@ -53,8 +57,20 @@ public FileUploadPropertyValueEditor(
IsAllowedInDataTypeConfiguration));
}

public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
// the stored property value (if any) is the path to the file; convert it to the client model (FileUploadValue)
var propertyValue = property.GetValue(culture, segment);
return propertyValue is string stringValue
? new FileUploadValue
{
Src = stringValue
}
: null;
}

/// <summary>
/// Converts the value received from the editor into the value can be stored in the database.
/// Converts the client model (FileUploadValue) into the value can be stored in the database (the file path).
/// </summary>
/// <param name="editorValue">The value received from the editor.</param>
/// <param name="currentValue">The current value of the property</param>
Expand All @@ -63,47 +79,36 @@ public FileUploadPropertyValueEditor(
/// <para>The <paramref name="currentValue" /> is used to re-use the folder, if possible.</para>
/// <para>
/// The <paramref name="editorValue" /> is value passed in from the editor. If the value is empty, we
/// must delete the currently selected file (<paramref name="currentValue" />). If the value is not empty,
/// it is assumed to contain a temporary file key, and we will attempt to replace the currently selected
/// file with the corresponding temporary file.
/// must delete the currently selected file (<paramref name="currentValue" />).
/// </para>
/// </remarks>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
var currentStringValue = currentValue as string;
currentStringValue = currentStringValue.NullOrWhiteSpaceAsNull();

var editorStringValue = editorValue.Value as string;
editorStringValue = editorStringValue.NullOrWhiteSpaceAsNull();
FileUploadValue? editorModelValue = ParseFileUploadValue(editorValue.Value);

// no change?
if (editorStringValue == currentStringValue)
if (editorModelValue?.TemporaryFileId.HasValue is not true)
{
return currentValue;
}

var currentPath = currentStringValue;
if (currentPath.IsNullOrWhiteSpace() == false)
{
currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath);
}
// the current editor value (if any) is the path to the file
var currentPath = currentValue is string currentStringValue
&& currentStringValue.IsNullOrWhiteSpace() is false
? _mediaFileManager.FileSystem.GetRelativePath(currentStringValue)
: null;

// resetting the current value?
if (editorStringValue is null && currentPath.IsNullOrWhiteSpace() is false)
if (editorModelValue?.Src is null && currentPath.IsNullOrWhiteSpace() is false)
{
// delete the current file and clear the value of this property
_mediaFileManager.FileSystem.DeleteFile(currentPath);
return null;
}

// uploading a file?
if (Guid.TryParse(editorStringValue, out Guid temporaryFileKey) == false)
{
return editorStringValue;
}

TemporaryFileModel? file = TryGetTemporaryFile(temporaryFileKey);
if (file == null)
Guid? temporaryFileKey = editorModelValue?.TemporaryFileId;
TemporaryFileModel? file = temporaryFileKey is null ? null : TryGetTemporaryFile(temporaryFileKey.Value);
if (file is null)
{
// at this point the temporary file *should* have been validated by TemporaryFileUploadValidator, so we
// should never end up here. In case we do, let's attempt to at least be non-destructive by returning
Expand All @@ -113,7 +118,7 @@ public FileUploadPropertyValueEditor(

// schedule temporary file for deletion
using IScope scope = _scopeProvider.CreateScope();
_temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey, _scopeProvider);
_temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey!.Value, _scopeProvider);

// ensure we have the required guids
Guid contentKey = editorValue.ContentKey;
Expand All @@ -139,13 +144,23 @@ public FileUploadPropertyValueEditor(

scope.Complete();

return filepath == null ? null : _mediaFileManager.FileSystem.GetUrl(filepath);
return filepath is null ? null : _mediaFileManager.FileSystem.GetUrl(filepath);
}

private FileUploadValue? ParseFileUploadValue(object? editorValue)
{
if (editorValue is null)
{
return null;
}

return _jsonSerializer.TryDeserialize(editorValue, out FileUploadValue? modelValue)
? modelValue
: throw new ArgumentException($"Could not parse editor value to a {nameof(FileUploadValue)} object.");
}

private Guid? TryParseTemporaryFileKey(object? editorValue)
=> editorValue is string stringValue && Guid.TryParse(stringValue, out Guid temporaryFileKey)
? temporaryFileKey
: null;
=> ParseFileUploadValue(editorValue)?.TemporaryFileId;

private TemporaryFileModel? TryGetTemporaryFile(Guid temporaryFileKey)
=> _temporaryFileService.GetAsync(temporaryFileKey).GetAwaiter().GetResult();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.

using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
Expand Down Expand Up @@ -131,7 +130,7 @@ public ImageCropperPropertyValueEditor(
currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath);
}

ImageCropperValue? editorImageCropperValue = TryParseImageCropperValue(editorValue.Value);
ImageCropperValue? editorImageCropperValue = ParseImageCropperValue(editorValue.Value);

// ensure we have the required guids
Guid contentKey = editorValue.ContentKey;
Expand All @@ -149,7 +148,7 @@ public ImageCropperPropertyValueEditor(
using IScope scope = _scopeProvider.CreateScope();

TemporaryFileModel? file = null;
Guid? temporaryFileKey = TryParseTemporaryFileKey(editorImageCropperValue);
Guid? temporaryFileKey = editorImageCropperValue?.TemporaryFileId;
if (temporaryFileKey.HasValue)
{
file = TryGetTemporaryFile(temporaryFileKey.Value);
Expand Down Expand Up @@ -196,6 +195,7 @@ public ImageCropperPropertyValueEditor(
}

editorImageCropperValue.Src = filepath is null ? string.Empty : _mediaFileManager.FileSystem.GetUrl(filepath);
editorImageCropperValue.TemporaryFileId = null;
return _jsonSerializer.Serialize(editorImageCropperValue);
}

Expand All @@ -220,40 +220,20 @@ public override string ConvertDbToString(IPropertyType propertyType, object? val
return _jsonSerializer.Serialize(new { src = val, crops });
}

private ImageCropperValue? TryParseImageCropperValue(object? editorValue)
private ImageCropperValue? ParseImageCropperValue(object? editorValue)
{
try
{
if (editorValue is null ||
_jsonSerializer.TryDeserialize(editorValue, out ImageCropperValue? imageCropperValue) is false)
{
return null;
}

imageCropperValue.Prune();
return imageCropperValue;
}
catch (Exception ex)
if (editorValue is null)
{
// For some reason the value is invalid - log error and continue as if no value was saved
_logger.LogWarning(ex, "Could not parse editor value to an ImageCropperValue object.");
return null;
}

return null;
return _jsonSerializer.TryDeserialize(editorValue, out ImageCropperValue? imageCropperValue)
? imageCropperValue
: throw new ArgumentException($"Could not parse editor value to a {nameof(ImageCropperValue)} object.");
}

private Guid? TryParseTemporaryFileKey(object? editorValue)
{
ImageCropperValue? imageCropperValue = TryParseImageCropperValue(editorValue);
return imageCropperValue != null
? TryParseTemporaryFileKey(imageCropperValue)
: null;
}

private Guid? TryParseTemporaryFileKey(ImageCropperValue? editorValue)
=> Guid.TryParse(editorValue?.Src, out Guid temporaryFileKey)
? temporaryFileKey
: null;
=> ParseImageCropperValue(editorValue)?.TemporaryFileId;

private TemporaryFileModel? TryGetTemporaryFile(Guid temporaryFileKey)
=> _temporaryFileService.GetAsync(temporaryFileKey).GetAwaiter().GetResult();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;

public sealed class FileUploadValue : TemporaryFileUploadValueBase
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// Represents a value of the image cropper value editor.
/// </summary>
public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValue>
public class ImageCropperValue :TemporaryFileUploadValueBase, IHtmlEncodedString, IEquatable<ImageCropperValue>
{
/// <summary>
/// Gets or sets the value source image.
/// </summary>
public string? Src { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the value focal point.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text.Json.Serialization;

namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;

public abstract class TemporaryFileUploadValueBase
{
/// <summary>
/// Gets or sets the temporary file identifier that will replace an an existing <see cref="Src"/> value.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Guid? TemporaryFileId { get; set; }

/// <summary>
/// Gets or sets the value source image.
/// </summary>
public string? Src { get; set; } = string.Empty;

protected bool Equals(TemporaryFileUploadValueBase other) => Nullable.Equals(TemporaryFileId, other.TemporaryFileId) && Src == other.Src;

public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

if (obj.GetType() != this.GetType())
{
return false;
}

return Equals((TemporaryFileUploadValueBase)obj);
}

public override int GetHashCode() => HashCode.Combine(TemporaryFileId, Src);
}