Files
Umbraco-CMS/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs
2021-09-08 12:07:05 +02:00

302 lines
13 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
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.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
{
/// <summary>
/// Represents an image cropper property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.ImageCropper,
"Image Cropper",
"imagecropper",
ValueType = ValueTypes.Json,
HideLabel = false,
Group = Constants.PropertyEditors.Groups.Media,
Icon = "icon-crop")]
public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
INotificationHandler<ContentCopiedNotification>, INotificationHandler<ContentDeletedNotification>,
INotificationHandler<MediaDeletedNotification>, INotificationHandler<MediaSavingNotification>,
INotificationHandler<MemberDeletedNotification>
{
private readonly MediaFileManager _mediaFileManager;
private readonly ContentSettings _contentSettings;
private readonly IDataTypeService _dataTypeService;
private readonly IIOHelper _ioHelper;
private readonly UploadAutoFillProperties _autoFillProperties;
private readonly ILogger<ImageCropperPropertyEditor> _logger;
private readonly IContentService _contentService;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCropperPropertyEditor"/> class.
/// </summary>
public ImageCropperPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
ILoggerFactory loggerFactory,
MediaFileManager mediaFileManager,
IOptions<ContentSettings> contentSettings,
IDataTypeService dataTypeService,
IIOHelper ioHelper,
UploadAutoFillProperties uploadAutoFillProperties,
IContentService contentService)
: base(dataValueEditorFactory)
{
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
_contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings));
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_autoFillProperties = uploadAutoFillProperties ?? throw new ArgumentNullException(nameof(uploadAutoFillProperties));
_contentService = contentService;
_logger = loggerFactory.CreateLogger<ImageCropperPropertyEditor>();
}
public bool TryGetMediaPath(string propertyEditorAlias, object value, out string mediaPath)
{
if (propertyEditorAlias == Alias &&
GetFileSrcFromPropertyValue(value, out _, false) is var mediaPathValue &&
!string.IsNullOrWhiteSpace(mediaPathValue))
{
mediaPath = mediaPathValue;
return true;
}
mediaPath = null;
return false;
}
/// <summary>
/// Creates the corresponding property value editor.
/// </summary>
/// <returns>The corresponding property value editor.</returns>
protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create<ImageCropperPropertyValueEditor>(Attribute);
/// <summary>
/// Creates the corresponding preValue editor.
/// </summary>
/// <returns>The corresponding preValue editor.</returns>
protected override IConfigurationEditor CreateConfigurationEditor() => new ImageCropperConfigurationEditor(_ioHelper);
/// <summary>
/// Gets a value indicating whether a property is an image cropper field.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>
/// <c>true</c> if the specified property is an image cropper field; otherwise, <c>false</c>.
/// </returns>
private static bool IsCropperField(IProperty property) => property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.ImageCropper;
/// <summary>
/// Parses the property value into a json object.
/// </summary>
/// <param name="value">The property value.</param>
/// <param name="writeLog">A value indicating whether to log the error.</param>
/// <returns>The json object corresponding to the property value.</returns>
/// <remarks>In case of an error, optionally logs the error and returns null.</remarks>
private JObject GetJObject(string value, bool writeLog)
{
if (string.IsNullOrWhiteSpace(value))
return null;
try
{
return JsonConvert.DeserializeObject<JObject>(value);
}
catch (Exception ex)
{
if (writeLog)
_logger.LogError(ex, "Could not parse image cropper value '{Json}'", value);
return null;
}
}
/// <summary>
/// The paths to all image cropper property files contained within a collection of content entities
/// </summary>
/// <param name="entities"></param>
private IEnumerable<string> ContainedFilePaths(IEnumerable<IContentBase> entities) => entities
.SelectMany(x => x.Properties)
.Where(IsCropperField)
.SelectMany(GetFilePathsFromPropertyValues)
.Distinct();
/// <summary>
/// Look through all property values stored against the property and resolve any file paths stored
/// </summary>
/// <param name="prop"></param>
/// <returns></returns>
private IEnumerable<string> GetFilePathsFromPropertyValues(IProperty prop)
{
//parses out the src from a json string
foreach (var propertyValue in prop.Values)
{
//check if the published value contains data and return it
var src = GetFileSrcFromPropertyValue(propertyValue.PublishedValue, out var _);
if (src != null) yield return _mediaFileManager.FileSystem.GetRelativePath(src);
//check if the edited value contains data and return it
src = GetFileSrcFromPropertyValue(propertyValue.EditedValue, out var _);
if (src != null) yield return _mediaFileManager.FileSystem.GetRelativePath(src);
}
}
/// <summary>
/// Returns the "src" property from the json structure if the value is formatted correctly
/// </summary>
/// <param name="propVal"></param>
/// <param name="deserializedValue">The deserialized <see cref="JObject"/> value</param>
/// <param name="relative">Should the path returned be the application relative path</param>
/// <returns></returns>
private string GetFileSrcFromPropertyValue(object propVal, out JObject deserializedValue, bool relative = true)
{
deserializedValue = null;
if (propVal == null || !(propVal is string str)) return null;
if (!str.DetectIsJson())
{
// Assume the value is a plain string with the file path
deserializedValue = new JObject()
{
{ "src", str }
};
}
else
{
deserializedValue = GetJObject(str, true);
}
if (deserializedValue?["src"] == null) return null;
var src = deserializedValue["src"].Value<string>();
return relative ? _mediaFileManager.FileSystem.GetRelativePath(src) : src;
}
/// <summary>
/// After a content has been copied, also copy uploaded files.
/// </summary>
public void Handle(ContentCopiedNotification notification)
{
// get the image cropper field properties
var properties = notification.Original.Properties.Where(IsCropperField);
// copy files
var isUpdated = false;
foreach (var property in properties)
{
//copy each of the property values (variants, segments) to the destination by using the edited value
foreach (var propertyValue in property.Values)
{
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
var src = GetFileSrcFromPropertyValue(propVal, out var jo);
if (src == null)
{
continue;
}
var sourcePath = _mediaFileManager.FileSystem.GetRelativePath(src);
var copyPath = _mediaFileManager.CopyFile(notification.Copy, property.PropertyType, sourcePath);
jo["src"] = _mediaFileManager.FileSystem.GetUrl(copyPath);
notification.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.Culture, propertyValue.Segment);
isUpdated = true;
}
}
// if updated, re-save the copy with the updated value
if (isUpdated)
{
_contentService.Save(notification.Copy);
}
}
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
private void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
{
var filePathsToDelete = ContainedFilePaths(deletedEntities);
_mediaFileManager.DeleteMediaFiles(filePathsToDelete);
}
public void Handle(MediaSavingNotification notification)
{
foreach (var entity in notification.SavedEntities)
{
AutoFillProperties(entity);
}
}
/// <summary>
/// Auto-fill properties (or clear).
/// </summary>
private void AutoFillProperties(IContentBase model)
{
var properties = model.Properties.Where(IsCropperField);
foreach (var property in properties)
{
var autoFillConfig = _contentSettings.GetConfig(property.Alias);
if (autoFillConfig == null) continue;
foreach (var pvalue in property.Values)
{
var svalue = property.GetValue(pvalue.Culture, pvalue.Segment) as string;
if (string.IsNullOrWhiteSpace(svalue))
{
_autoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment);
}
else
{
var jo = GetJObject(svalue, false);
string src;
if (jo == null)
{
// so we have a non-empty string value that cannot be parsed into a json object
// see http://issues.umbraco.org/issue/U4-4756
// it can happen when an image is uploaded via the folder browser, in which case
// the property value will be the file source eg '/media/23454/hello.jpg' and we
// are fixing that anomaly here - does not make any sense at all but... bah...
var dt = _dataTypeService.GetDataType(property.PropertyType.DataTypeId);
var config = dt?.ConfigurationAs<ImageCropperConfiguration>();
src = svalue;
var json = new
{
src = svalue,
crops = config == null ? Array.Empty<ImageCropperConfiguration.Crop>() : config.Crops
};
property.SetValue(JsonConvert.SerializeObject(json), pvalue.Culture, pvalue.Segment);
}
else
{
src = jo["src"]?.Value<string>();
}
if (src == null)
_autoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment);
else
_autoFillProperties.Populate(model, autoFillConfig, _mediaFileManager.FileSystem.GetRelativePath(src), pvalue.Culture, pvalue.Segment);
}
}
}
}
}
}