// 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
{
///
/// Represents an image cropper property editor.
///
[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, INotificationHandler,
INotificationHandler, INotificationHandler,
INotificationHandler
{
private readonly MediaFileManager _mediaFileManager;
private readonly ContentSettings _contentSettings;
private readonly IDataTypeService _dataTypeService;
private readonly IIOHelper _ioHelper;
private readonly UploadAutoFillProperties _autoFillProperties;
private readonly ILogger _logger;
private readonly IContentService _contentService;
///
/// Initializes a new instance of the class.
///
public ImageCropperPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
ILoggerFactory loggerFactory,
MediaFileManager mediaFileManager,
IOptions 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();
}
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;
}
///
/// Creates the corresponding property value editor.
///
/// The corresponding property value editor.
protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute);
///
/// Creates the corresponding preValue editor.
///
/// The corresponding preValue editor.
protected override IConfigurationEditor CreateConfigurationEditor() => new ImageCropperConfigurationEditor(_ioHelper);
///
/// Gets a value indicating whether a property is an image cropper field.
///
/// The property.
///
/// true if the specified property is an image cropper field; otherwise, false.
///
private static bool IsCropperField(IProperty property) => property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.ImageCropper;
///
/// Parses the property value into a json object.
///
/// The property value.
/// A value indicating whether to log the error.
/// The json object corresponding to the property value.
/// In case of an error, optionally logs the error and returns null.
private JObject GetJObject(string value, bool writeLog)
{
if (string.IsNullOrWhiteSpace(value))
return null;
try
{
return JsonConvert.DeserializeObject(value);
}
catch (Exception ex)
{
if (writeLog)
_logger.LogError(ex, "Could not parse image cropper value '{Json}'", value);
return null;
}
}
///
/// The paths to all image cropper property files contained within a collection of content entities
///
///
private IEnumerable ContainedFilePaths(IEnumerable entities) => entities
.SelectMany(x => x.Properties)
.Where(IsCropperField)
.SelectMany(GetFilePathsFromPropertyValues)
.Distinct();
///
/// Look through all property values stored against the property and resolve any file paths stored
///
///
///
private IEnumerable 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);
}
}
///
/// Returns the "src" property from the json structure if the value is formatted correctly
///
///
/// The deserialized value
/// Should the path returned be the application relative path
///
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();
return relative ? _mediaFileManager.FileSystem.GetRelativePath(src) : src;
}
///
/// After a content has been copied, also copy uploaded files.
///
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 deletedEntities)
{
var filePathsToDelete = ContainedFilePaths(deletedEntities);
_mediaFileManager.DeleteMediaFiles(filePathsToDelete);
}
public void Handle(MediaSavingNotification notification)
{
foreach (var entity in notification.SavedEntities)
{
AutoFillProperties(entity);
}
}
///
/// Auto-fill properties (or clear).
///
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();
src = svalue;
var json = new
{
src = svalue,
crops = config == null ? Array.Empty() : config.Crops
};
property.SetValue(JsonConvert.SerializeObject(json), pvalue.Culture, pvalue.Segment);
}
else
{
src = jo["src"]?.Value();
}
if (src == null)
_autoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment);
else
_autoFillProperties.Populate(model, autoFillConfig, _mediaFileManager.FileSystem.GetRelativePath(src), pvalue.Culture, pvalue.Segment);
}
}
}
}
}
}