Remove Json.NET dependencies for more property editors (#13912)

* Make the Tags property editor JSON serialization agnostic

* Make the Multiple Value editor JSON serializer agnostic

* Make Media Picker 3 value converter work with System.Text.Json instead of Json.NET

* Make Image Cropper value converter work with System.Text.Json instead of Json.NET

* Remove Json.NET dependencies from ImageCropperTemplateExtensions

* Update src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
This commit is contained in:
Kenn Jacobsen
2023-03-07 14:31:27 +01:00
committed by GitHub
parent 3b77debbfb
commit 1b8ce54e49
8 changed files with 218 additions and 285 deletions

View File

@@ -1,10 +1,9 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -26,6 +25,7 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
private readonly IDataTypeService _dataTypeService;
private readonly ILogger<ImageCropperPropertyValueEditor> _logger;
private readonly MediaFileManager _mediaFileManager;
private readonly IJsonSerializer _jsonSerializer;
private ContentSettings _contentSettings;
public ImageCropperPropertyValueEditor(
@@ -42,6 +42,7 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mediaFileManager = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
_jsonSerializer = jsonSerializer;
_contentSettings = contentSettings.CurrentValue;
_dataTypeService = dataTypeService;
contentSettings.OnChange(x => _contentSettings = x);
@@ -62,7 +63,7 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
ImageCropperValue? value;
try
{
value = JsonConvert.DeserializeObject<ImageCropperValue>(val.ToString()!);
value = _jsonSerializer.Deserialize<ImageCropperValue>(val.ToString()!);
}
catch
{
@@ -97,17 +98,16 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
var currentPath = string.Empty;
try
{
var svalue = currentValue as string;
JObject? currentJson = string.IsNullOrWhiteSpace(svalue) ? null : JObject.Parse(svalue);
if (currentJson != null && currentJson.TryGetValue("src", out JToken? src))
if (currentValue is string currentStringValue)
{
currentPath = src.Value<string>();
ImageCropperValue? currentImageCropperValue = _jsonSerializer.Deserialize<ImageCropperValue>(currentStringValue);
currentPath = currentImageCropperValue?.Src;
}
}
catch (Exception ex)
{
// For some reason the value is invalid so continue as if there was no value there
_logger.LogWarning(ex, "Could not parse current db value to a JObject.");
_logger.LogWarning(ex, "Could not parse current db value to an ImageCropperValue object.");
}
if (string.IsNullOrWhiteSpace(currentPath) == false)
@@ -115,23 +115,21 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath);
}
// Get the new JSON and file path
var editorFile = string.Empty;
var editorJson = (JObject?)editorValue.Value;
if (editorJson is not null)
{
// Populate current file
if (editorJson["src"] != null)
{
editorFile = editorJson["src"]?.Value<string>();
}
ImageCropperValue? editorImageCropperValue = null;
// Clean up redundant/default data
ImageCropperValue.Prune(editorJson);
}
else
// FIXME: consider creating an object deserialization method on IJsonSerializer instead of relying on deserializing serialized JSON here (and likely other places as well)
if (editorValue.Value is JsonObject jsonObject)
{
editorJson = null;
try
{
editorImageCropperValue = _jsonSerializer.Deserialize<ImageCropperValue>(jsonObject.ToJsonString());
editorImageCropperValue?.Prune();
}
catch (Exception ex)
{
// 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.");
}
}
// ensure we have the required guids
@@ -163,17 +161,17 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
// if editorFile is empty then either there was nothing to begin with,
// or it has been cleared and we need to remove the file - else the
// value is unchanged.
if (string.IsNullOrWhiteSpace(editorFile) && string.IsNullOrWhiteSpace(currentPath) == false)
if (string.IsNullOrWhiteSpace(editorImageCropperValue?.Src) && string.IsNullOrWhiteSpace(currentPath) is false)
{
_mediaFileManager.FileSystem.DeleteFile(currentPath);
return null; // clear
}
return editorJson?.ToString(Formatting.None); // unchanged
return _jsonSerializer.Serialize(editorImageCropperValue); // unchanged
}
// process the file
var filepath = editorJson == null ? null : ProcessFile(file, cuid, puid);
var filepath = editorImageCropperValue == null ? null : ProcessFile(file, cuid, puid);
// remove all temp files
foreach (ContentPropertyFile f in uploads)
@@ -188,13 +186,13 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
}
// update json and return
if (editorJson == null)
if (editorImageCropperValue == null)
{
return null;
}
editorJson["src"] = filepath == null ? string.Empty : _mediaFileManager.FileSystem.GetUrl(filepath);
return editorJson.ToString(Formatting.None);
editorImageCropperValue.Src = filepath is null ? string.Empty : _mediaFileManager.FileSystem.GetUrl(filepath);
return _jsonSerializer.Serialize(editorImageCropperValue);
}
public override string ConvertDbToString(IPropertyType propertyType, object? value)
@@ -216,9 +214,7 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
?.ConfigurationAs<ImageCropperConfiguration>();
ImageCropperConfiguration.Crop[] crops = configuration?.Crops ?? Array.Empty<ImageCropperConfiguration.Crop>();
return JsonConvert.SerializeObject(
new { src = val, crops },
new JsonSerializerSettings { Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore });
return _jsonSerializer.Serialize(new { src = val, crops });
}
private string? ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid)

View File

@@ -1,8 +1,5 @@
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -126,23 +123,29 @@ public class MediaPicker3PropertyEditor : DataEditor
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (editorValue.Value is JArray dtos)
// FIXME: consider creating an object deserialization method on IJsonSerializer instead of relying on deserializing serialized JSON here (and likely other places as well)
if (editorValue.Value is not JsonArray jsonArray)
{
if (editorValue.DataTypeConfiguration is MediaPicker3Configuration configuration)
{
dtos = PersistTempMedia(dtos, configuration);
}
// Clean up redundant/default data
foreach (JObject? dto in dtos.Values<JObject>())
{
MediaWithCropsDto.Prune(dto);
}
return dtos.ToString(Formatting.None);
return base.FromEditor(editorValue, currentValue);
}
return base.FromEditor(editorValue, currentValue);
List<MediaWithCropsDto>? mediaWithCropsDtos = _jsonSerializer.Deserialize<List<MediaWithCropsDto>>(jsonArray.ToJsonString());
if (mediaWithCropsDtos is null)
{
return base.FromEditor(editorValue, currentValue);
}
if (editorValue.DataTypeConfiguration is MediaPicker3Configuration configuration)
{
// FIXME: handle temp files here once we implement file uploads (see old implementation "PersistTempMedia" in the commented out code below)
}
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
{
mediaWithCropsDto.Prune();
}
return _jsonSerializer.Serialize(mediaWithCropsDtos);
}
internal static IEnumerable<MediaWithCropsDto> Deserialize(IJsonSerializer jsonSerializer, object? value)
@@ -185,76 +188,77 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
private JArray PersistTempMedia(JArray jArray, MediaPicker3Configuration mediaPicker3Configuration)
{
var result = new JArray();
foreach (JObject? dto in jArray.Values<JObject>())
{
if (dto is null)
{
continue;
}
if (!dto.TryGetValue("tmpLocation", out JToken? temporaryLocation))
{
// If it does not have a temporary path, it can be an already saved image or not-yet uploaded temp-image, check for media-key
if (dto.TryGetValue("mediaKey", out _))
{
result.Add(dto);
}
continue;
}
var temporaryLocationString = temporaryLocation.Value<string>();
if (temporaryLocationString is null)
{
continue;
}
GuidUdi? startNodeGuid = mediaPicker3Configuration.StartNodeId as GuidUdi ?? null;
JToken? mediaTypeAlias = dto.GetValue("mediaTypeAlias");
IMedia mediaFile = _temporaryMediaService.Save(temporaryLocationString, startNodeGuid?.Guid, mediaTypeAlias?.Value<string>());
MediaWithCropsDto? mediaDto = _jsonSerializer.Deserialize<MediaWithCropsDto>(dto.ToString());
if (mediaDto is null)
{
continue;
}
mediaDto.MediaKey = mediaFile.GetUdi().Guid;
result.Add(JObject.Parse(_jsonSerializer.Serialize(mediaDto)));
}
return result;
}
// private JArray PersistTempMedia(JArray jArray, MediaPicker3Configuration mediaPicker3Configuration)
// {
// var result = new JArray();
// foreach (JObject? dto in jArray.Values<JObject>())
// {
// if (dto is null)
// {
// continue;
// }
//
// if (!dto.TryGetValue("tmpLocation", out JToken? temporaryLocation))
// {
// // If it does not have a temporary path, it can be an already saved image or not-yet uploaded temp-image, check for media-key
// if (dto.TryGetValue("mediaKey", out _))
// {
// result.Add(dto);
// }
//
// continue;
// }
//
// var temporaryLocationString = temporaryLocation.Value<string>();
// if (temporaryLocationString is null)
// {
// continue;
// }
//
// GuidUdi? startNodeGuid = mediaPicker3Configuration.StartNodeId as GuidUdi ?? null;
// JToken? mediaTypeAlias = dto.GetValue("mediaTypeAlias");
// IMedia mediaFile = _temporaryMediaService.Save(temporaryLocationString, startNodeGuid?.Guid, mediaTypeAlias?.Value<string>());
// MediaWithCropsDto? mediaDto = _jsonSerializer.Deserialize<MediaWithCropsDto>(dto.ToString());
// if (mediaDto is null)
// {
// continue;
// }
//
// mediaDto.MediaKey = mediaFile.GetUdi().Guid;
// result.Add(JObject.Parse(_jsonSerializer.Serialize(mediaDto)));
// }
//
// return result;
// }
/// <summary>
/// Model/DTO that represents the JSON that the MediaPicker3 stores.
/// </summary>
[DataContract]
internal class MediaWithCropsDto
{
[DataMember(Name = "key")]
public Guid Key { get; set; }
[DataMember(Name = "mediaKey")]
public Guid MediaKey { get; set; }
[DataMember(Name = "crops")]
public IEnumerable<ImageCropperValue.ImageCropperCrop>? Crops { get; set; }
[DataMember(Name = "focalPoint")]
public ImageCropperValue.ImageCropperFocalPoint? FocalPoint { get; set; }
/// <summary>
/// Removes redundant crop data/default focal point.
/// </summary>
/// <param name="value">The media with crops DTO.</param>
/// <remarks>
/// Because the DTO uses the same JSON keys as the image cropper value for crops and focal point, we can re-use the
/// prune method.
/// </remarks>
public static void Prune(JObject? value) => ImageCropperValue.Prune(value);
internal void Prune()
{
Crops = Crops?.Where(crop => crop.Coordinates != null).ToArray();
if (FocalPoint is { Top: 0.5m, Left: 0.5m })
{
FocalPoint = null;
}
}
/// <summary>
/// Applies the configuration to ensure only valid crops are kept and have the correct width/height.

View File

@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -20,15 +18,16 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </remarks>
public class MultipleValueEditor : DataValueEditor
{
private readonly IJsonSerializer _jsonSerializer;
public MultipleValueEditor(
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
}
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) =>
_jsonSerializer = jsonSerializer;
/// <summary>
/// Override so that we can return an array to the editor for multi-select values
@@ -43,7 +42,7 @@ public class MultipleValueEditor : DataValueEditor
string[]? result = null;
if (json is not null)
{
result = JsonConvert.DeserializeObject<string[]>(json);
result = _jsonSerializer.Deserialize<string[]>(json);
}
return result ?? Array.Empty<string>();
@@ -58,17 +57,12 @@ public class MultipleValueEditor : DataValueEditor
/// <returns></returns>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (editorValue.Value is not JArray json || json.HasValues == false)
if (editorValue.Value is not IEnumerable<string> stringValues || stringValues.Any() == false)
{
return null;
}
var values = json.Select(item => item.Value<string>()).ToArray();
if (values.Length == 0)
{
return null;
}
return JsonConvert.SerializeObject(values, Formatting.None);
var result = _jsonSerializer.Serialize(stringValues);
return result;
}
}

View File

@@ -3,8 +3,6 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -96,6 +94,7 @@ public class TagsPropertyEditor : DataEditor
internal class TagPropertyValueEditor : DataValueEditor, IDataValueTags
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IDataTypeService _dataTypeService;
public TagPropertyValueEditor(
@@ -107,19 +106,41 @@ public class TagsPropertyEditor : DataEditor
IDataTypeService dataTypeService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
_dataTypeService = dataTypeService;
}
/// <inheritdoc />
public IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
var strValue = value?.ToString();
if (string.IsNullOrWhiteSpace(strValue)) return Enumerable.Empty<ITag>();
TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(dataTypeConfiguration) ?? new TagConfiguration();
var tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(dataTypeConfiguration) ?? new TagConfiguration();
var tags = ParseTags(value, tagConfiguration);
if (tags.Any() is false)
{
return Enumerable.Empty<ITag>();
}
return tags.Select(x => new Tag
{
Group = tagConfiguration.Group,
Text = x,
LanguageId = languageId,
});
}
private string[] ParseTags(object? value, TagConfiguration tagConfiguration)
{
var strValue = value?.ToString();
if (string.IsNullOrWhiteSpace(strValue))
{
return Array.Empty<string>();
}
if (tagConfiguration.Delimiter == default)
{
tagConfiguration.Delimiter = ',';
}
IEnumerable<string> tags;
@@ -132,9 +153,9 @@ public class TagsPropertyEditor : DataEditor
case TagsStorageType.Json:
try
{
tags = JsonConvert.DeserializeObject<string[]>(strValue)?.Select(x => x.Trim()) ?? Enumerable.Empty<string>();
tags = _jsonSerializer.Deserialize<string[]>(strValue)?.Select(x => x.Trim()) ?? Enumerable.Empty<string>();
}
catch (JsonException)
catch
{
//cannot parse, malformed
tags = Enumerable.Empty<string>();
@@ -146,46 +167,45 @@ public class TagsPropertyEditor : DataEditor
throw new NotSupportedException($"Value \"{tagConfiguration.StorageType}\" is not a valid TagsStorageType.");
}
return tags.Select(x => new Tag
{
Group = tagConfiguration.Group,
Text = x,
LanguageId = languageId,
});
return tags.ToArray();
}
/// <inheritdoc />
public override IValueRequiredValidator RequiredValidator => new RequiredJsonValueValidator();
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var val = property.GetValue(culture, segment);
if (val is null)
{
return null;
}
IDataType? dataType = _dataTypeService.GetDataType(property.PropertyType.DataTypeId);
TagConfiguration configuration = dataType?.ConfigurationObject as TagConfiguration ?? new TagConfiguration();
var tags = ParseTags(val, configuration);
return tags.Any() ? tags : null;
}
/// <inheritdoc />
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
var value = editorValue.Value?.ToString();
if (string.IsNullOrEmpty(value))
if (editorValue.Value is not IEnumerable<string> stringValues)
{
return null;
}
var tagConfiguration = editorValue.DataTypeConfiguration as TagConfiguration ?? new TagConfiguration();
var trimmedTags = stringValues.Select(s => s.Trim()).Where(s => s.IsNullOrWhiteSpace() == false).ToArray();
if (trimmedTags.Any() is false)
{
return null;
}
TagConfiguration tagConfiguration = editorValue.DataTypeConfiguration as TagConfiguration ?? new TagConfiguration();
if (tagConfiguration.Delimiter == default)
{
tagConfiguration.Delimiter = ',';
string[] trimmedTags = Array.Empty<string>();
if (editorValue.Value is JArray json)
{
trimmedTags = json.HasValues ? json.Select(x => x.Value<string>()).OfType<string>().ToArray() : Array.Empty<string>();
}
else if (string.IsNullOrWhiteSpace(value) == false)
{
trimmedTags = value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
}
if (trimmedTags.Length == 0)
{
return null;
}
switch (tagConfiguration.StorageType)
@@ -194,7 +214,7 @@ public class TagsPropertyEditor : DataEditor
return string.Join(tagConfiguration.Delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull();
case TagsStorageType.Json:
return trimmedTags.Length == 0 ? null : JsonConvert.SerializeObject(trimmedTags);
return trimmedTags.Length == 0 ? null : _jsonSerializer.Serialize(trimmedTags);
}
return null;

View File

@@ -1,14 +1,9 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -16,35 +11,27 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// Represents a value of the image cropper value editor.
/// </summary>
[JsonConverter(typeof(NoTypeConverterJsonConverter<ImageCropperValue>))]
[TypeConverter(typeof(ImageCropperValueTypeConverter))]
[DataContract(Name = "imageCropDataSet")]
public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValue>
{
/// <summary>
/// Gets or sets the value source image.
/// </summary>
[DataMember(Name = "src")]
public string? Src { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the value focal point.
/// </summary>
[DataMember(Name = "focalPoint")]
public ImageCropperFocalPoint? FocalPoint { get; set; }
/// <summary>
/// Gets or sets the value crops.
/// </summary>
[DataMember(Name = "crops")]
public IEnumerable<ImageCropperCrop>? Crops { get; set; }
/// <inheritdoc />
public string? ToHtmlString() => Src;
/// <inheritdoc />
public override string? ToString()
=> HasCrops() || HasFocalPoint() ? JsonConvert.SerializeObject(this, Formatting.None) : Src;
public override string? ToString() => Src;
/// <summary>
/// Gets a crop.
@@ -185,56 +172,20 @@ public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValu
/// <summary>
/// Removes redundant crop data/default focal point.
/// </summary>
/// <param name="value">The image cropper value.</param>
public static void Prune(JObject? value)
internal void Prune()
{
if (value is null)
Crops = Crops?.Where(crop => crop.Coordinates != null).ToArray();
if (FocalPoint is { Top: 0.5m, Left: 0.5m })
{
throw new ArgumentNullException(nameof(value));
}
if (value.TryGetValue("crops", out JToken? crops))
{
if (crops.HasValues)
{
foreach (JObject crop in crops.Values<JObject>().WhereNotNull().ToList())
{
if (crop.TryGetValue("coordinates", out JToken? coordinates) == false ||
coordinates.HasValues == false)
{
// Remove crop without coordinates
crop.Remove();
continue;
}
// Width/height are already stored in the crop configuration
crop.Remove("width");
crop.Remove("height");
}
}
if (crops.HasValues == false)
{
// Remove empty crops
value.Remove("crops");
}
}
if (value.TryGetValue("focalPoint", out JToken? focalPoint) &&
(focalPoint.HasValues == false ||
(focalPoint.Value<decimal>("top") == 0.5m && focalPoint.Value<decimal>("left") == 0.5m)))
{
// Remove empty/default focal point
value.Remove("focalPoint");
FocalPoint = null;
}
}
[DataContract(Name = "imageCropFocalPoint")]
public class ImageCropperFocalPoint : IEquatable<ImageCropperFocalPoint>
{
[DataMember(Name = "left")] public decimal Left { get; set; }
public decimal Left { get; set; }
[DataMember(Name = "top")] public decimal Top { get; set; }
public decimal Top { get; set; }
#region IEquatable
@@ -272,16 +223,15 @@ public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValu
#endregion
}
[DataContract(Name = "imageCropData")]
public class ImageCropperCrop : IEquatable<ImageCropperCrop>
{
[DataMember(Name = "alias")] public string? Alias { get; set; }
public string? Alias { get; set; }
[DataMember(Name = "width")] public int Width { get; set; }
public int Width { get; set; }
[DataMember(Name = "height")] public int Height { get; set; }
public int Height { get; set; }
[DataMember(Name = "coordinates")] public ImageCropperCropCoordinates? Coordinates { get; set; }
public ImageCropperCropCoordinates? Coordinates { get; set; }
#region IEquatable
@@ -325,16 +275,15 @@ public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValu
#endregion
}
[DataContract(Name = "imageCropCoordinates")]
public class ImageCropperCropCoordinates : IEquatable<ImageCropperCropCoordinates>
{
[DataMember(Name = "x1")] public decimal X1 { get; set; }
public decimal X1 { get; set; }
[DataMember(Name = "y1")] public decimal Y1 { get; set; }
public decimal Y1 { get; set; }
[DataMember(Name = "x2")] public decimal X2 { get; set; }
public decimal X2 { get; set; }
[DataMember(Name = "y2")] public decimal Y2 { get; set; }
public decimal Y2 { get; set; }
#region IEquatable
@@ -357,7 +306,7 @@ public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValu
public static bool operator ==(ImageCropperCropCoordinates? left, ImageCropperCropCoordinates? right)
=> Equals(left, right);
public static bool operator !=(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
public static bool operator !=(ImageCropperCropCoordinates? left, ImageCropperCropCoordinates? right)
=> !Equals(left, right);
public override int GetHashCode()

View File

@@ -1,40 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel;
using System.Globalization;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// Converts <see cref="ImageCropperValue" /> to string or JObject (why?).
/// </summary>
public class ImageCropperValueTypeConverter : TypeConverter
{
private static readonly Type[] _convertableTypes = { typeof(JObject) };
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
if (destinationType is null)
{
return false;
}
return _convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType))
|| CanConvertFrom(context, destinationType);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is not ImageCropperValue cropperValue)
{
return null;
}
return TypeHelper.IsTypeAssignableFrom<JObject>(destinationType)
? JObject.FromObject(cropperValue)
: base.ConvertTo(context, culture, value, destinationType);
}
}

View File

@@ -1,8 +1,9 @@
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Extensions;
@@ -11,12 +12,6 @@ namespace Umbraco.Extensions;
/// </summary>
public static class ImageCropperTemplateExtensions
{
private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new()
{
Culture = CultureInfo.InvariantCulture,
FloatParseHandling = FloatParseHandling.Decimal,
};
internal static ImageCropperValue DeserializeImageCropperValue(this string json)
{
ImageCropperValue? imageCrops = null;
@@ -25,8 +20,8 @@ public static class ImageCropperTemplateExtensions
{
try
{
imageCrops =
JsonConvert.DeserializeObject<ImageCropperValue>(json, ImageCropperValueJsonSerializerSettings);
IJsonSerializer? serializer = StaticServiceProvider.Instance.GetService<IJsonSerializer>();
imageCrops = serializer?.Deserialize<ImageCropperValue>(json);
}
catch (Exception ex)
{

View File

@@ -1,17 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common;
@@ -30,6 +28,8 @@ public class ImageCropperTest
[Test]
public void CanConvertImageCropperDataSetSrcToString()
{
SetupJsonSerializerServiceProvider();
// cropperJson3 - has no crops
var cropperValue = CropperJson2.DeserializeImageCropperValue();
var serialized = cropperValue.TryConvertTo<string>();
@@ -38,30 +38,38 @@ public class ImageCropperTest
}
[Test]
public void CanConvertImageCropperDataSetJObject()
public void CanConvertJsonStringToImageCropperValue()
{
// cropperJson3 - has no crops
var cropperValue = CropperJson2.DeserializeImageCropperValue();
var serialized = cropperValue.TryConvertTo<JObject>();
Assert.IsTrue(serialized.Success);
Assert.AreEqual(cropperValue, serialized.Result.ToObject<ImageCropperValue>());
SetupJsonSerializerServiceProvider();
// cropperJson1 - has crops
var cropperValue = CropperJson1.DeserializeImageCropperValue();
Assert.AreEqual(MediaPath, cropperValue.Src);
Assert.IsNotNull(cropperValue.FocalPoint);
Assert.AreEqual(0.96m, cropperValue.FocalPoint.Left);
Assert.AreEqual(0.80827067669172936m, cropperValue.FocalPoint.Top);
Assert.IsNotNull(cropperValue.Crops);
Assert.AreEqual(1, cropperValue.Crops.Count());
var crop = cropperValue.Crops.First();
Assert.AreEqual("thumb", crop.Alias);
Assert.AreEqual(100, crop.Width);
Assert.AreEqual(100, crop.Height);
Assert.IsNotNull(crop.Coordinates);
Assert.AreEqual(0.58729977382575338m, crop.Coordinates.X1);
Assert.AreEqual(0.055768992440203169m, crop.Coordinates.Y1);
Assert.AreEqual(0m, crop.Coordinates.X2);
Assert.AreEqual(0.32457553600198386m, crop.Coordinates.Y2);
}
[Test]
public void CanConvertImageCropperDataSetJsonToString()
{
SetupJsonSerializerServiceProvider();
var cropperValue = CropperJson1.DeserializeImageCropperValue();
var serialized = cropperValue.TryConvertTo<string>();
Assert.IsTrue(serialized.Success);
Assert.IsTrue(serialized.Result.DetectIsJson());
var obj = JsonConvert.DeserializeObject<ImageCropperValue>(
CropperJson1,
new JsonSerializerSettings
{
Culture = CultureInfo.InvariantCulture,
FloatParseHandling = FloatParseHandling.Decimal,
});
Assert.AreEqual(cropperValue, obj);
Assert.AreEqual("/media/1005/img_0671.jpg", serialized.Result);
}
// [TestCase(CropperJson1, CropperJson1, true)]
@@ -416,6 +424,13 @@ public class ImageCropperTest
Assert.AreEqual(MediaPath + "?m=pad&w=400&h=400&bgcolor=fff", urlString);
}
private void SetupJsonSerializerServiceProvider()
{
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(s => s.GetService(typeof(IJsonSerializer))).Returns(new SystemTextJsonSerializer());
StaticServiceProvider.Instance = serviceProvider.Object;
}
internal class TestImageUrlGenerator : IImageUrlGenerator
{
public IEnumerable<string> SupportedImageFileTypes => new[] { "jpeg", "jpg", "gif", "bmp", "png", "tiff", "tif" };